From 9b40db05bfc64e12fd270de09386ec0ad48bbc72 Mon Sep 17 00:00:00 2001 From: Michael Buesch Date: Sun, 27 Oct 2024 23:38:00 +0100 Subject: [PATCH] Detect and block clients that violate the protocol --- letmein-fwproto/src/lib.rs | 21 ++++- letmeind/src/firewall_client.rs | 28 ++++++- letmeind/src/main.rs | 14 +++- letmeind/src/monitor.rs | 122 ++++++++++++++++++++++++++++ letmeinfwd/src/firewall.rs | 4 + letmeinfwd/src/firewall/nftables.rs | 12 ++- letmeinfwd/src/server.rs | 29 ++++++- 7 files changed, 221 insertions(+), 9 deletions(-) create mode 100644 letmeind/src/monitor.rs diff --git a/letmein-fwproto/src/lib.rs b/letmein-fwproto/src/lib.rs index 98b95b1..a328948 100644 --- a/letmein-fwproto/src/lib.rs +++ b/letmein-fwproto/src/lib.rs @@ -35,6 +35,8 @@ pub enum FirewallOperation { Ack, /// Open a port. Open, + /// Block an address. + BlockAddr, } impl TryFrom for FirewallOperation { @@ -44,10 +46,12 @@ impl TryFrom for FirewallOperation { const OPERATION_OPEN: u16 = FirewallOperation::Open as u16; const OPERATION_ACK: u16 = FirewallOperation::Ack as u16; const OPERATION_NACK: u16 = FirewallOperation::Nack as u16; + const OPERATION_BLOCKADDR: u16 = FirewallOperation::BlockAddr as u16; match value { OPERATION_OPEN => Ok(Self::Open), OPERATION_ACK => Ok(Self::Ack), OPERATION_NACK => Ok(Self::Nack), + OPERATION_BLOCKADDR => Ok(Self::BlockAddr), _ => Err(err!("Invalid FirewallMessage/Operation value")), } } @@ -207,6 +211,17 @@ impl FirewallMessage { } } + /// Construct a new message that requests installing a firewall-block-address rule. + pub fn new_block_addr(addr: IpAddr) -> Self { + let (addr_type, addr) = addr_to_octets(addr); + Self { + operation: FirewallOperation::BlockAddr, + addr_type, + addr, + ..Default::default() + } + } + /// Get the operation type from this message. pub fn operation(&self) -> FirewallOperation { self.operation @@ -216,14 +231,16 @@ impl FirewallMessage { pub fn port(&self) -> Option<(PortType, u16)> { match self.operation { FirewallOperation::Open => Some((self.port_type, self.port)), - FirewallOperation::Ack | FirewallOperation::Nack => None, + FirewallOperation::Ack | FirewallOperation::Nack | FirewallOperation::BlockAddr => None, } } /// Get the `IpAddr` from this message. pub fn addr(&self) -> Option { match self.operation { - FirewallOperation::Open => Some(octets_to_addr(self.addr_type, &self.addr)), + FirewallOperation::Open | FirewallOperation::BlockAddr => { + Some(octets_to_addr(self.addr_type, &self.addr)) + } FirewallOperation::Ack | FirewallOperation::Nack => None, } } diff --git a/letmeind/src/firewall_client.rs b/letmeind/src/firewall_client.rs index 0d5f1f3..babaa21 100644 --- a/letmeind/src/firewall_client.rs +++ b/letmeind/src/firewall_client.rs @@ -51,7 +51,33 @@ impl FirewallClient { match msg_reply.operation() { FirewallOperation::Ack => Ok(()), FirewallOperation::Nack => Err(err!("The firewall rejected the port-open request")), - FirewallOperation::Open => Err(err!("Received invalid reply")), + FirewallOperation::Open | FirewallOperation::BlockAddr => { + Err(err!("Received invalid reply")) + } + } + } + + pub async fn block_addr(&mut self, addr: IpAddr) -> ah::Result<()> { + // Send an block-address request to the firewall daemon. + FirewallMessage::new_block_addr(addr) + .send(&mut self.stream) + .await + .context("Send block-addr message")?; + + // Receive the block-addr reply. + let Some(msg_reply) = FirewallMessage::recv(&mut self.stream) + .await + .context("Receive block-addr reply")? + else { + return Err(err!("Connection terminated")); + }; + + match msg_reply.operation() { + FirewallOperation::Ack => Ok(()), + FirewallOperation::Nack => Err(err!("The firewall rejected the block-addr request")), + FirewallOperation::Open | FirewallOperation::BlockAddr => { + Err(err!("Received invalid reply")) + } } } } diff --git a/letmeind/src/main.rs b/letmeind/src/main.rs index fd53a2f..2935ae4 100644 --- a/letmeind/src/main.rs +++ b/letmeind/src/main.rs @@ -12,10 +12,11 @@ std::compile_error!("letmeind server does not support non-Linux platforms."); mod firewall_client; +mod monitor; mod protocol; mod server; -use crate::{protocol::Protocol, server::Server}; +use crate::{monitor::Monitor, protocol::Protocol, server::Server}; use anyhow::{self as ah, format_err as err, Context as _}; use clap::Parser; use letmein_conf::{Config, ConfigVariant, Seccomp}; @@ -189,6 +190,9 @@ async fn async_main(opts: Arc) -> ah::Result<()> { // Create async IPC channels. let (exit_sock_tx, mut exit_sock_rx) = sync::mpsc::channel(1); + // Create client monitor. + let monitor = Arc::new(Monitor::new(&opts.rundir)); + // Start the TCP control port listener. let srv = Server::new(&*conf.read().await, opts.no_systemd) .await @@ -205,12 +209,15 @@ async fn async_main(opts: Arc) -> ah::Result<()> { task::spawn({ let conf = Arc::clone(&conf); let opts = Arc::clone(&opts); + let monitor = Arc::clone(&monitor); async move { let conn_semaphore = Semaphore::new(opts.num_connections); loop { let conf = Arc::clone(&conf); let opts = Arc::clone(&opts); + let monitor = Arc::clone(&monitor); + match srv.accept().await { Ok(conn) => { // Socket connection handler. @@ -218,8 +225,11 @@ async fn async_main(opts: Arc) -> ah::Result<()> { task::spawn(async move { let conf = conf.read().await; let mut proto = Protocol::new(conn, &conf, &opts.rundir); + let ipaddr = proto.addr().ip(); if let Err(e) = proto.run().await { - eprintln!("Client '{}' ERROR: {}", proto.addr().ip(), e); + monitor.log_client_error(ipaddr, e).await; + } else { + monitor.log_client_auth_ok(ipaddr).await; } }); } diff --git a/letmeind/src/monitor.rs b/letmeind/src/monitor.rs new file mode 100644 index 0000000..fada9b3 --- /dev/null +++ b/letmeind/src/monitor.rs @@ -0,0 +1,122 @@ +// -*- coding: utf-8 -*- +// +// Copyright (C) 2024 Michael Büsch +// +// Licensed under the Apache License version 2.0 +// or the MIT license, at your option. +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use crate::firewall_client::FirewallClient; +use anyhow as ah; +use std::{ + collections::HashMap, + net::IpAddr, + path::{Path, PathBuf}, + time::{Duration, Instant}, +}; +use tokio::sync::Mutex; + +const MON_ERROR_THRES: u32 = 3; +const MON_TIMEOUT: Duration = Duration::from_millis(1000 * 60 * 60 * 12); + +struct MonitorData { + errors: u32, + timeout: Instant, +} + +impl MonitorData { + fn new() -> Self { + Self { + errors: 0, + timeout: Instant::now() + MON_TIMEOUT, + } + } +} + +struct MonitorInner { + rundir: PathBuf, + clients: HashMap, +} + +impl MonitorInner { + fn new(rundir: PathBuf) -> Self { + Self { + rundir, + clients: HashMap::new(), + } + } + + async fn maintenance(&mut self) { + // Remove timed-out monitoring entries. + let now = Instant::now(); + self.clients.retain(|_, mon_data| mon_data.timeout < now); + } + + async fn log_client_error(&mut self, addr: IpAddr, error: ah::Error) { + eprintln!("Client '{addr}' ERROR: {error}"); + + // Remove timed-out monitoring entries. + self.maintenance().await; + + // Get the monitoring state of this client. + let mon_data = self.clients.entry(addr).or_insert_with(MonitorData::new); + + // Increment the error count. + mon_data.errors = mon_data.errors.saturating_add(1); + + // If the error count is above a threshold, block the client. + if mon_data.errors >= MON_ERROR_THRES { + eprintln!("Monitor: Blocking client {addr}"); + + // Connect to letmeinfwd unix socket. + let mut fw = match FirewallClient::new(&self.rundir).await { + Ok(fw) => fw, + Err(e) => { + eprintln!("Monitor: Failed to connect to `letmeinfwd`: {e}"); + return; + } + }; + + // Block the IP address in the firewall. + if let Err(e) = fw.block_addr(addr).await { + eprintln!("Monitor: Failed block client {addr} via `letmeinfwd`: {e}"); + return; + } + + // The client has been blocked in the firewall. + // Do not track it in the monitoring any longer. + self.clients.remove(&addr); + } + } + + async fn log_client_auth_ok(&mut self, addr: IpAddr) { + // The client has successfully authenticated. + // Do not track it in the monitoring any longer. + self.clients.remove(&addr); + + // Remove timed-out monitoring entries. + self.maintenance().await; + } +} + +pub struct Monitor { + inner: Mutex, +} + +impl Monitor { + pub fn new(rundir: &Path) -> Self { + Self { + inner: Mutex::new(MonitorInner::new(rundir.to_path_buf())), + } + } + + pub async fn log_client_error(&self, addr: IpAddr, error: ah::Error) { + self.inner.lock().await.log_client_error(addr, error).await; + } + + pub async fn log_client_auth_ok(&self, addr: IpAddr) { + self.inner.lock().await.log_client_auth_ok(addr).await; + } +} + +// vim: ts=4 sw=4 expandtab diff --git a/letmeinfwd/src/firewall.rs b/letmeinfwd/src/firewall.rs index f099731..1790c7c 100644 --- a/letmeinfwd/src/firewall.rs +++ b/letmeinfwd/src/firewall.rs @@ -161,4 +161,8 @@ pub trait FirewallOpen { ) -> ah::Result<()>; } +pub trait FirewallBlock { + async fn block_addr(&mut self, conf: &Config, remote_addr: IpAddr) -> ah::Result<()>; +} + // vim: ts=4 sw=4 expandtab diff --git a/letmeinfwd/src/firewall/nftables.rs b/letmeinfwd/src/firewall/nftables.rs index da816ba..8f219cc 100644 --- a/letmeinfwd/src/firewall/nftables.rs +++ b/letmeinfwd/src/firewall/nftables.rs @@ -7,8 +7,8 @@ // SPDX-License-Identifier: Apache-2.0 OR MIT use crate::firewall::{ - prune_all_lease_timeouts, FirewallMaintain, FirewallOpen, Lease, LeaseMap, LeasePort, - SingleLeasePort, + prune_all_lease_timeouts, FirewallBlock, FirewallMaintain, FirewallOpen, Lease, LeaseMap, + LeasePort, SingleLeasePort, }; use anyhow::{self as ah, format_err as err, Context as _}; use letmein_conf::Config; @@ -472,4 +472,12 @@ impl FirewallOpen for NftFirewall { } } +impl FirewallBlock for NftFirewall { + async fn block_addr(&mut self, conf: &Config, remote_addr: IpAddr) -> ah::Result<()> { + assert!(!self.shutdown); + //TODO + Ok(()) + } +} + // vim: ts=4 sw=4 expandtab diff --git a/letmeinfwd/src/server.rs b/letmeinfwd/src/server.rs index dad02a9..f5acb70 100644 --- a/letmeinfwd/src/server.rs +++ b/letmeinfwd/src/server.rs @@ -7,7 +7,7 @@ // SPDX-License-Identifier: Apache-2.0 OR MIT use crate::{ - firewall::{FirewallOpen, LeasePort}, + firewall::{FirewallBlock, FirewallOpen, LeasePort}, set_owner_mode, LETMEIND_GID, LETMEIND_UID, }; use anyhow::{self as ah, format_err as err, Context as _}; @@ -76,7 +76,7 @@ impl FirewallConnection { pub async fn handle_message( &mut self, conf: &Config, - fw: Arc>, + fw: Arc>, ) -> ah::Result<()> { let Some(msg) = self.recv_msg().await? else { return Err(err!("Disconnected.")); @@ -136,6 +136,31 @@ impl FirewallConnection { self.send_msg(&FirewallMessage::new_nack()).await?; } } + FirewallOperation::BlockAddr => { + // Get the address from the socket message. + let Some(addr) = msg.addr() else { + self.send_msg(&FirewallMessage::new_nack()).await?; + return Err(err!("No addr.")); + }; + + // Check if addr is valid. + if !addr_check(&addr) { + self.send_msg(&FirewallMessage::new_nack()).await?; + return Err(err!("Invalid addr.")); + } + + // Block the address in the firewall. + let ok = { + let mut fw = fw.lock().await; + fw.block_addr(conf, addr).await.is_ok() + }; + + if ok { + self.send_msg(&FirewallMessage::new_ack()).await?; + } else { + self.send_msg(&FirewallMessage::new_nack()).await?; + } + } FirewallOperation::Ack | FirewallOperation::Nack => { return Err(err!("Received invalid message")); }