1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
use crate::error::NtraceError;
use crate::services::{PROTOCOL_SIGNATURES, SERVICE_PROBES, get_service_name};
use log::{debug, trace};
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::time::timeout;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Protocol {
Tcp,
Udp,
// Add more protocols
}
/// Represents a scan target, which can be either an IP address or a domain name
#[derive(Debug, Clone, PartialEq)]
pub enum Target {
/// An IP address (IPv4 or IPv6)
Ip(std::net::IpAddr),
/// A domain name (e.g., "wikipedia.org")
Domain(String),
}
#[derive(Clone)]
pub struct ProtocolAnalyzer {
// Timeout for service detection probes
probe_timeout: Duration,
}
impl ProtocolAnalyzer {
pub fn new() -> Self {
ProtocolAnalyzer {
probe_timeout: Duration::from_millis(500),
}
}
/// Create a new analyzer with a custom probe timeout
pub fn with_timeout(probe_timeout: Duration) -> Self {
ProtocolAnalyzer { probe_timeout }
}
/// Detects the service running on the port via banner grabbing and probing.
pub async fn detect_service(
&self,
stream: &mut TcpStream,
protocol: Protocol,
) -> Result<Option<String>, NtraceError> {
match protocol {
Protocol::Tcp => {
// Try to get initial banner without sending anything
let service = self.try_banner_grab(stream).await?;
if service.is_some() {
return Ok(service);
}
// If no banner, try active probing
self.try_service_probes(stream).await
}
Protocol::Udp => {
// UDP service detection based on port number and responses
let port = match stream.peer_addr() {
Ok(addr) => addr.port(),
Err(_) => return Ok(None),
};
// Common UDP services by port
let service = match port {
53 => Some("domain"),
67 | 68 => Some("dhcp"),
69 => Some("tftp"),
123 => Some("ntp"),
161 => Some("snmp"),
162 => Some("snmptrap"),
500 => Some("isakmp"),
514 => Some("syslog"),
520 => Some("rip"),
1194 => Some("openvpn"),
1900 => Some("upnp"),
5353 => Some("mdns"),
_ => None,
};
if let Some(svc) = service {
return Ok(Some(svc.to_string()));
}
// For unknown ports, check the well known services database
let default_service = get_service_name(port);
if default_service != "unknown" {
return Ok(Some(default_service.to_string()));
}
Ok(None)
}
}
}
/// Tries to grab a banner without sending any data
async fn try_banner_grab(&self, stream: &mut TcpStream) -> Result<Option<String>, NtraceError> {
let mut buffer = [0; 2048];
// Set read timeout
match timeout(self.probe_timeout, stream.read(&mut buffer)).await {
Ok(Ok(n)) if n > 0 => {
let banner = String::from_utf8_lossy(&buffer[..n]).into_owned();
trace!("Received banner: {}", banner);
// Check for known protocol signatures
for (pattern, protocol_name) in PROTOCOL_SIGNATURES.iter() {
if banner.contains(pattern) {
return Ok(Some(protocol_name.to_string()));
}
}
// No known signature found
Ok(None)
}
_ => Ok(None),
}
}
/// Tries different service probes to identify the service
async fn try_service_probes(
&self,
stream: &mut TcpStream,
) -> Result<Option<String>, NtraceError> {
// Get the port number for well known service lookup
let port = match stream.peer_addr() {
Ok(addr) => addr.port(),
Err(_) => return Ok(None),
};
// First check if this is a well known port
let default_service = get_service_name(port);
if default_service != "unknown" {
debug!(
"Detected service on port {}: {} (by port number)",
port, default_service
);
return Ok(Some(default_service.to_string()));
}
// Try each probe
for probe in SERVICE_PROBES.iter() {
// Clone the stream for each probe attempt
if let Ok(mut probe_stream) = TcpStream::connect(stream.peer_addr()?).await {
debug!("Trying {} probe on port {}", probe.name, port);
// Send the probe data
if probe_stream.write_all(probe.probe_data).await.is_ok()
&& probe_stream.flush().await.is_ok()
{
let mut buffer = [0; 2048];
// Wait for response with timeout
match timeout(self.probe_timeout, probe_stream.read(&mut buffer)).await {
Ok(Ok(n)) if n > 0 => {
let response = String::from_utf8_lossy(&buffer[..n]);
if response.contains(probe.signature) {
debug!(
"Detected service on port {}: {} (by probe)",
port, probe.name
);
return Ok(Some(probe.name.to_string()));
}
}
_ => continue,
}
}
}
}
// No service detected
Ok(None)
}
/// Analyzes protocol details to determine version and features
pub async fn analyze_protocol(
&self,
stream: &TcpStream,
protocol: Protocol,
) -> Result<Option<String>, NtraceError> {
match protocol {
Protocol::Tcp => {
// Get the port number
let port = match stream.peer_addr() {
Ok(addr) => addr.port(),
Err(_) => return Ok(None),
};
// Try to determine protocol details based on port
match port {
80 | 8080 | 8000 => self.analyze_http(stream).await,
443 | 8443 => self.analyze_tls(stream).await,
22 => self.analyze_ssh(stream).await,
21 => self.analyze_ftp(stream).await,
25 | 587 => self.analyze_smtp(stream).await,
_ => Ok(None),
}
}
Protocol::Udp => {
// Get the port number
let port = match stream.peer_addr() {
Ok(addr) => addr.port(),
Err(_) => return Ok(None),
};
// Analyze UDP protocol based on port
match port {
53 => self.analyze_dns().await,
123 => self.analyze_ntp().await,
161 | 162 => self.analyze_snmp().await,
500 => self.analyze_ipsec().await,
1194 => self.analyze_openvpn().await,
_ => Ok(Some("UDP".to_string())),
}
}
}
}
/// Analyze HTTP protocol details
async fn analyze_http(&self, _stream: &TcpStream) -> Result<Option<String>, NtraceError> {
// In a production version, this would connect and determine HTTP version, server type, etc.
Ok(Some("HTTP".to_string()))
}
/// Analyze TLS/SSL protocol details
async fn analyze_tls(&self, _stream: &TcpStream) -> Result<Option<String>, NtraceError> {
// In a production version, this would determine TLS version, cipher suites, etc.
Ok(Some("TLS/SSL".to_string()))
}
/// Analyze SSH protocol details
async fn analyze_ssh(&self, _stream: &TcpStream) -> Result<Option<String>, NtraceError> {
// In a production version, this would determine SSH version, supported auth methods, etc.
Ok(Some("SSH".to_string()))
}
/// Analyze FTP protocol details
async fn analyze_ftp(&self, _stream: &TcpStream) -> Result<Option<String>, NtraceError> {
// In a production version, this would determine FTP version, supported commands, etc.
Ok(Some("FTP".to_string()))
}
/// Analyze SMTP protocol details
async fn analyze_smtp(&self, _stream: &TcpStream) -> Result<Option<String>, NtraceError> {
// In a production version, this would determine SMTP version, supported extensions, etc.
Ok(Some("SMTP".to_string()))
}
/// Analyze DNS protocol details (UDP)
async fn analyze_dns(&self) -> Result<Option<String>, NtraceError> {
// In a production version, this would determine DNS server type, version, etc.
Ok(Some("DNS".to_string()))
}
/// Analyze NTP protocol details (UDP)
async fn analyze_ntp(&self) -> Result<Option<String>, NtraceError> {
// In a production version, this would determine NTP version, mode, etc.
Ok(Some("NTP".to_string()))
}
/// Analyze SNMP protocol details (UDP)
async fn analyze_snmp(&self) -> Result<Option<String>, NtraceError> {
// In a production version, this would determine SNMP version, community strings, etc.
Ok(Some("SNMP".to_string()))
}
/// Analyze IPsec/ISAKMP protocol details (UDP)
async fn analyze_ipsec(&self) -> Result<Option<String>, NtraceError> {
// In a production version, this would determine IPsec version, supported transforms, etc.
Ok(Some("IPsec/ISAKMP".to_string()))
}
/// Analyze OpenVPN protocol details (UDP)
async fn analyze_openvpn(&self) -> Result<Option<String>, NtraceError> {
// In a production version, this would determine OpenVPN version, etc.
Ok(Some("OpenVPN".to_string()))
}
}