Skip to main content

netcore/
service.rs

1//! Listening services and active flows.
2//!
3//! `Service` describes a listening socket plus its effective exposure to the
4//! outside world, computed from the bind scope and the firewall posture.
5//! `Flow` is an established connection.
6
7use std::net::{IpAddr, SocketAddr};
8
9use serde::{Deserialize, Serialize};
10
11use crate::diag::FirewallVerdict;
12use crate::link::{L4Proto, TcpState};
13use crate::process::ProcessInfo;
14
15/// Something listening for inbound connections.
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17pub struct Service {
18    /// Local port the socket is listening on.
19    pub port: u16,
20    pub proto: L4Proto,
21    pub bind: BindScope,
22    pub process: ProcessInfo,
23    /// Effective reachability, computed by joining [`BindScope`] with the
24    /// firewall's inbound verdict for (port, proto).
25    pub exposure: Exposure,
26}
27
28/// Where a listening socket is bound.
29///
30/// A Linux `[::]:22` listener accepts IPv4 connections as well unless the
31/// `net.ipv6.bindv6only` sysctl is set — [`BindScope::AnyAddress`] is the
32/// right variant in that dual-stack case.
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34#[serde(tag = "type", content = "value", rename_all = "snake_case")]
35pub enum BindScope {
36    AnyAddress,
37    Loopback,
38    /// `%iface` suffix on the bind address (common with link-local IPv6).
39    SpecificInterface(String),
40    /// A specific non-loopback address (e.g. `127.0.0.53` for the systemd
41    /// stub, or a LAN IP when the process bound to one).
42    SpecificAddress(IpAddr),
43}
44
45/// How widely a listening service is reachable.
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
47#[serde(rename_all = "snake_case")]
48pub enum Exposure {
49    /// Bound to loopback — only reachable from this host.
50    LocalOnly,
51    /// Firewall blocks WAN but LAN peers can reach the service.
52    LanOnly,
53    /// Reachable from any peer that can reach this host.
54    Exposed,
55    /// Firewall state unknown (no [`Firewall`](crate::Firewall) backend).
56    Unknown,
57}
58
59impl Exposure {
60    /// Combine a [`BindScope`] with a [`FirewallVerdict`] into an [`Exposure`].
61    ///
62    /// Loopback-bound services are always `LocalOnly` regardless of firewall.
63    /// A specific-address bind on a non-loopback address is treated like
64    /// any-address for firewall purposes (the firewall still governs WAN).
65    pub fn from_scope_and_verdict(bind: &BindScope, verdict: FirewallVerdict) -> Self {
66        match bind {
67            BindScope::Loopback => Exposure::LocalOnly,
68            BindScope::SpecificAddress(ip) if ip.is_loopback() => Exposure::LocalOnly,
69            _ => match verdict {
70                FirewallVerdict::Allow => Exposure::Exposed,
71                FirewallVerdict::Drop | FirewallVerdict::Reject => Exposure::LanOnly,
72                FirewallVerdict::NoMatch | FirewallVerdict::Unknown => Exposure::Unknown,
73            },
74        }
75    }
76}
77
78/// An established or in-flight connection to/from this host.
79#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
80pub struct Flow {
81    pub proto: L4Proto,
82    /// Local socket address.
83    pub local: SocketAddr,
84    /// Remote peer socket address.
85    pub remote: SocketAddr,
86    pub state: TcpState,
87    pub process: ProcessInfo,
88    /// Bytes received on this flow.
89    pub bytes_in: u64,
90    /// Bytes sent on this flow.
91    pub bytes_out: u64,
92    /// Smoothed round-trip time in microseconds, when the kernel has one
93    /// (TCP only, and only once the connection has carried data).
94    #[serde(default, skip_serializing_if = "Option::is_none")]
95    pub rtt_us: Option<u32>,
96}