mirror of
https://github.com/domcyrus/rustnet.git
synced 2026-01-05 13:29:55 -06:00
implement option to filter for state
This commit is contained in:
24
README.md
24
README.md
@@ -143,11 +143,35 @@ Press `/` to enter filter mode. Type to filter connections in real-time, navigat
|
||||
- `dst:github.com` - Destinations containing "github.com"
|
||||
- `process:ssh` - Process names containing "ssh"
|
||||
- `sni:api` - SNI hostnames containing "api"
|
||||
- `state:established` - Filter connections by protocol state
|
||||
|
||||
**State filtering:**
|
||||
|
||||
Filter connections by their current protocol state (case-insensitive):
|
||||
|
||||
⚠️ **Note:** State tracking accuracy varies by protocol. TCP states are most reliable, while UDP, QUIC, and other protocol states are derived from packet inspection and internal lifecycle management, which may not always reflect the true connection state.
|
||||
|
||||
- `state:syn_recv` - Show half-open connections (useful for detecting SYN floods)
|
||||
- `state:established` - Show only established connections
|
||||
- `state:fin_wait` - Show connections in closing states
|
||||
- `state:quic_handshake` - Show QUIC connections during handshake
|
||||
- `state:dns_query` - Show DNS query connections
|
||||
- `state:udp_active` - Show active UDP connections
|
||||
|
||||
**Available states:**
|
||||
- **TCP**: `SYN_SENT`, `SYN_RECV`, `ESTABLISHED`, `FIN_WAIT1`, `FIN_WAIT2`, `TIME_WAIT`, `CLOSE_WAIT`, `LAST_ACK`, `CLOSING`, `CLOSED`
|
||||
- **QUIC**: `QUIC_INITIAL`, `QUIC_HANDSHAKE`, `QUIC_CONNECTED`, `QUIC_DRAINING`, `QUIC_CLOSED` ⚠️ *Note: QUIC state tracking may be incomplete due to encrypted handshake packets and reassembly challenges*
|
||||
- **UDP**: `UDP_ACTIVE`, `UDP_IDLE`, `UDP_STALE`
|
||||
- **DNS**: `DNS_QUERY`, `DNS_RESPONSE`
|
||||
- **Other**: `ECHO_REQUEST`, `ECHO_REPLY`, `ARP_REQUEST`, `ARP_REPLY`
|
||||
|
||||
**Examples:**
|
||||
|
||||
- `sport:80 process:nginx` - Nginx connections from port 80
|
||||
- `dport:443 sni:google.com` - HTTPS connections to Google
|
||||
- `sport:443 state:syn_recv` - Half-open connections to port 443 (SYN flood detection)
|
||||
- `proto:tcp state:established` - All established TCP connections
|
||||
- `process:firefox state:quic_connected` - Active QUIC connections from Firefox
|
||||
|
||||
Press `Esc` to clear filter.
|
||||
|
||||
|
||||
127
src/filter.rs
127
src/filter.rs
@@ -24,6 +24,8 @@ pub enum FilterCriteria {
|
||||
Sni(String),
|
||||
/// Match DPI application protocol
|
||||
Application(String),
|
||||
/// Match connection state (e.g., ESTABLISHED, SYN_RECV)
|
||||
State(String),
|
||||
}
|
||||
|
||||
pub struct ConnectionFilter {
|
||||
@@ -79,6 +81,9 @@ impl ConnectionFilter {
|
||||
"app" | "application" => {
|
||||
criteria.push(FilterCriteria::Application(value));
|
||||
}
|
||||
"state" => {
|
||||
criteria.push(FilterCriteria::State(value));
|
||||
}
|
||||
_ => {
|
||||
// Unknown keyword, treat as general search
|
||||
criteria.push(FilterCriteria::General(part.to_lowercase()));
|
||||
@@ -151,6 +156,9 @@ impl ConnectionFilter {
|
||||
}
|
||||
FilterCriteria::Sni(sni_text) => self.matches_sni(connection, sni_text),
|
||||
FilterCriteria::Application(app_text) => self.matches_application(connection, app_text),
|
||||
FilterCriteria::State(state_text) => {
|
||||
connection.state().to_lowercase().contains(state_text)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -372,4 +380,123 @@ mod tests {
|
||||
_ => panic!("Expected DestinationPort filter"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_state_filter() {
|
||||
let filter = ConnectionFilter::parse("state:established");
|
||||
assert_eq!(filter.criteria.len(), 1);
|
||||
match &filter.criteria[0] {
|
||||
FilterCriteria::State(text) => assert_eq!(text, "established"),
|
||||
_ => panic!("Expected State filter"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_state_filter_tcp_states() {
|
||||
use crate::network::types::*;
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
|
||||
// Create a test connection in ESTABLISHED state
|
||||
let mut conn = Connection::new(
|
||||
Protocol::TCP,
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 12345),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)), 80),
|
||||
ProtocolState::Tcp(TcpState::Established),
|
||||
);
|
||||
|
||||
// Test matching established state
|
||||
let established_filter = ConnectionFilter::parse("state:established");
|
||||
assert!(established_filter.matches(&conn));
|
||||
|
||||
// Test partial matching
|
||||
let est_filter = ConnectionFilter::parse("state:est");
|
||||
assert!(est_filter.matches(&conn));
|
||||
|
||||
// Test case insensitive matching
|
||||
let upper_filter = ConnectionFilter::parse("state:ESTABLISHED");
|
||||
assert!(upper_filter.matches(&conn));
|
||||
|
||||
// Test non-matching state
|
||||
let syn_filter = ConnectionFilter::parse("state:syn_recv");
|
||||
assert!(!syn_filter.matches(&conn));
|
||||
|
||||
// Change connection to SYN_RECV state
|
||||
conn.protocol_state = ProtocolState::Tcp(TcpState::SynReceived);
|
||||
assert!(syn_filter.matches(&conn));
|
||||
assert!(!established_filter.matches(&conn));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_state_filter_udp_states() {
|
||||
use crate::network::types::*;
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
|
||||
// Create a fresh UDP connection (should show as UDP_ACTIVE)
|
||||
let conn = Connection::new(
|
||||
Protocol::UDP,
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 12345),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8)), 53),
|
||||
ProtocolState::Udp,
|
||||
);
|
||||
|
||||
let active_filter = ConnectionFilter::parse("state:udp_active");
|
||||
assert!(active_filter.matches(&conn));
|
||||
|
||||
let udp_filter = ConnectionFilter::parse("state:udp");
|
||||
assert!(udp_filter.matches(&conn));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_combined_state_and_port_filter() {
|
||||
use crate::network::types::*;
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
|
||||
let conn = Connection::new(
|
||||
Protocol::TCP,
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 443),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)), 54321),
|
||||
ProtocolState::Tcp(TcpState::SynReceived),
|
||||
);
|
||||
|
||||
// Test combined filter: source port 443 AND SYN_RECV state
|
||||
let combined_filter = ConnectionFilter::parse("sport:443 state:syn_recv");
|
||||
assert!(combined_filter.matches(&conn));
|
||||
|
||||
// Test that both conditions must match
|
||||
let wrong_port_filter = ConnectionFilter::parse("sport:80 state:syn_recv");
|
||||
assert!(!wrong_port_filter.matches(&conn));
|
||||
|
||||
let wrong_state_filter = ConnectionFilter::parse("sport:443 state:established");
|
||||
assert!(!wrong_state_filter.matches(&conn));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_state_filter_case_insensitive() {
|
||||
use crate::network::types::*;
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
|
||||
let conn = Connection::new(
|
||||
Protocol::TCP,
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 12345),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)), 80),
|
||||
ProtocolState::Tcp(TcpState::Established),
|
||||
);
|
||||
|
||||
// Test various case combinations
|
||||
let filters = vec![
|
||||
"state:established",
|
||||
"state:ESTABLISHED",
|
||||
"state:Established",
|
||||
"state:EstAbLiShEd",
|
||||
];
|
||||
|
||||
for filter_str in filters {
|
||||
let filter = ConnectionFilter::parse(filter_str);
|
||||
assert!(
|
||||
filter.matches(&conn),
|
||||
"Filter '{}' should match ESTABLISHED state",
|
||||
filter_str
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user