1use crate::extractor::L4Proto;
39
40pub type Entry = (L4Proto, u16, &'static str);
43
44pub fn protocol_label(proto: L4Proto, src_port: u16, dst_port: u16) -> Option<&'static str> {
54 let table = table_for(proto)?;
55 let lower = match (src_port, dst_port) {
56 (0, 0) => return None,
57 (0, p) | (p, 0) => p,
58 (a, b) => a.min(b),
59 };
60 if let Some(label) = lookup(table, lower) {
61 return Some(label);
62 }
63 let higher = src_port.max(dst_port);
67 if higher != lower {
68 lookup(table, higher)
69 } else {
70 None
71 }
72}
73
74pub fn entries() -> impl Iterator<Item = Entry> {
78 TCP_TABLE
79 .iter()
80 .map(|(p, l)| (L4Proto::Tcp, *p, *l))
81 .chain(UDP_TABLE.iter().map(|(p, l)| (L4Proto::Udp, *p, *l)))
82}
83
84fn table_for(proto: L4Proto) -> Option<&'static [(u16, &'static str)]> {
85 match proto {
86 L4Proto::Tcp => Some(TCP_TABLE),
87 L4Proto::Udp => Some(UDP_TABLE),
88 _ => None,
89 }
90}
91
92fn lookup(table: &[(u16, &'static str)], port: u16) -> Option<&'static str> {
93 table
94 .binary_search_by_key(&port, |(p, _)| *p)
95 .ok()
96 .map(|i| table[i].1)
97}
98
99const TCP_TABLE: &[(u16, &str)] = &[
102 (20, "ftp-data"),
103 (21, "ftp"),
104 (22, "ssh"),
105 (23, "telnet"),
106 (25, "smtp"),
107 (53, "dns"),
108 (80, "http"),
109 (110, "pop3"),
110 (143, "imap"),
111 (443, "tls/https"),
112 (465, "smtps"),
113 (587, "smtp-submission"),
114 (853, "dns-over-tls"),
115 (993, "imaps"),
116 (995, "pop3s"),
117 (1433, "mssql"),
118 (1521, "oracle"),
119 (2049, "nfs"),
120 (3306, "mysql"),
121 (3389, "rdp"),
122 (5432, "postgres"),
123 (5672, "amqp"),
124 (5984, "couchdb"),
125 (6379, "redis"),
126 (6443, "kubernetes-api"),
127 (6667, "irc"),
128 (7000, "cassandra"),
129 (7001, "cassandra"),
130 (8000, "http"),
131 (8080, "http"),
132 (8088, "hbase"),
133 (8443, "tls/https"),
134 (8500, "consul"),
135 (9000, "minio"),
136 (9001, "minio"),
137 (9042, "cassandra-cql"),
138 (9092, "kafka"),
139 (9093, "kafka"),
140 (9200, "elasticsearch"),
141 (9300, "elasticsearch"),
142 (10000, "webmin"),
143 (11211, "memcached"),
144 (15672, "rabbitmq-mgmt"),
145 (27017, "mongodb"),
146 (50070, "hdfs"),
147];
148
149const UDP_TABLE: &[(u16, &str)] = &[
151 (53, "dns"),
152 (67, "dhcp"),
153 (68, "dhcp"),
154 (69, "tftp"),
155 (88, "kerberos"),
156 (123, "ntp"),
157 (137, "netbios"),
158 (138, "netbios"),
159 (139, "netbios"),
160 (161, "snmp"),
161 (162, "snmp"),
162 (389, "ldap"),
163 (443, "quic/http3"),
164 (500, "ipsec"),
165 (514, "syslog"),
166 (636, "ldaps"),
167 (853, "dns-over-quic"),
168 (1812, "radius"),
169 (1813, "radius"),
170 (2049, "nfs"),
171 (2152, "gtp-u"),
172 (3478, "stun"),
173 (4500, "ipsec"),
174 (4789, "vxlan"),
175 (5060, "sip"),
176 (5061, "sip"),
177];
178
179#[derive(Clone, Default, Debug)]
201pub struct LabelTable {
202 overrides: std::collections::HashMap<(L4Proto, u16), &'static str>,
203 inherit_builtin: bool,
207}
208
209impl LabelTable {
210 pub fn new() -> Self {
213 Self {
214 overrides: std::collections::HashMap::new(),
215 inherit_builtin: true,
216 }
217 }
218
219 pub fn standalone() -> Self {
222 Self {
223 overrides: std::collections::HashMap::new(),
224 inherit_builtin: false,
225 }
226 }
227
228 pub fn set(&mut self, proto: L4Proto, port: u16, label: &'static str) -> &mut Self {
230 self.overrides.insert((proto, port), label);
231 self
232 }
233
234 pub fn extend<I>(&mut self, entries: I) -> &mut Self
237 where
238 I: IntoIterator<Item = (L4Proto, u16, &'static str)>,
239 {
240 for (proto, port, label) in entries {
241 self.overrides.insert((proto, port), label);
242 }
243 self
244 }
245
246 pub fn lookup(&self, proto: L4Proto, src_port: u16, dst_port: u16) -> Option<&'static str> {
256 if let Some(label) = self.overrides.get(&(proto, src_port)) {
257 return Some(*label);
258 }
259 if let Some(label) = self.overrides.get(&(proto, dst_port)) {
260 return Some(*label);
261 }
262 if self.inherit_builtin {
263 protocol_label(proto, src_port, dst_port)
264 } else {
265 None
266 }
267 }
268
269 pub fn inherit_builtin(&self) -> bool {
272 self.inherit_builtin
273 }
274
275 pub fn remove(&mut self, proto: L4Proto, port: u16) -> Option<&'static str> {
283 self.overrides.remove(&(proto, port))
284 }
285
286 pub fn contains(&self, proto: L4Proto, port: u16) -> bool {
292 self.overrides.contains_key(&(proto, port))
293 }
294
295 pub fn len(&self) -> usize {
301 self.overrides.len()
302 }
303
304 pub fn is_empty(&self) -> bool {
311 self.overrides.is_empty()
312 }
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318
319 #[test]
320 fn tcp_sorted_ascending() {
321 for w in TCP_TABLE.windows(2) {
322 assert!(
323 w[0].0 < w[1].0,
324 "TCP_TABLE not sorted: {} >= {}",
325 w[0].0,
326 w[1].0
327 );
328 }
329 }
330
331 #[test]
332 fn udp_sorted_ascending() {
333 for w in UDP_TABLE.windows(2) {
334 assert!(
335 w[0].0 < w[1].0,
336 "UDP_TABLE not sorted: {} >= {}",
337 w[0].0,
338 w[1].0
339 );
340 }
341 }
342
343 #[test]
344 fn known_labels() {
345 assert_eq!(protocol_label(L4Proto::Tcp, 80, 33000), Some("http"));
346 assert_eq!(protocol_label(L4Proto::Tcp, 33000, 443), Some("tls/https"));
347 assert_eq!(protocol_label(L4Proto::Udp, 33000, 53), Some("dns"));
348 assert_eq!(protocol_label(L4Proto::Tcp, 33000, 6379), Some("redis"));
349 }
350
351 #[test]
352 fn lower_port_disambiguates_two_known() {
353 assert_eq!(protocol_label(L4Proto::Tcp, 80, 443), Some("http"));
355 assert_eq!(protocol_label(L4Proto::Tcp, 443, 80), Some("http"));
356 }
357
358 #[test]
359 fn higher_port_fallback_when_lower_unknown() {
360 assert_eq!(protocol_label(L4Proto::Tcp, 1024, 80), Some("http"));
364 }
365
366 #[test]
367 fn unknown_returns_none() {
368 assert_eq!(protocol_label(L4Proto::Tcp, 33000, 33001), None);
369 assert_eq!(protocol_label(L4Proto::Udp, 33000, 33001), None);
370 assert_eq!(protocol_label(L4Proto::Udp, 80, 33000), None);
372 }
373
374 #[test]
375 fn icmp_and_other_protocols_return_none() {
376 assert_eq!(protocol_label(L4Proto::Icmp, 0, 0), None);
377 assert_eq!(protocol_label(L4Proto::IcmpV6, 0, 0), None);
378 assert_eq!(protocol_label(L4Proto::Sctp, 80, 80), None);
379 assert_eq!(protocol_label(L4Proto::Other(99), 80, 80), None);
380 }
381
382 #[test]
383 fn zero_port_opts_out_of_that_side() {
384 assert_eq!(protocol_label(L4Proto::Tcp, 0, 80), Some("http"));
386 assert_eq!(protocol_label(L4Proto::Tcp, 80, 0), Some("http"));
387 assert_eq!(protocol_label(L4Proto::Tcp, 0, 0), None);
389 }
390
391 #[test]
392 fn entries_iterates_full_table() {
393 let count = entries().count();
394 assert_eq!(count, TCP_TABLE.len() + UDP_TABLE.len());
395 }
396
397 #[test]
398 fn entries_contains_known_rows() {
399 let v: Vec<_> = entries().collect();
400 assert!(v.contains(&(L4Proto::Tcp, 80, "http")));
401 assert!(v.contains(&(L4Proto::Udp, 53, "dns")));
402 assert!(v.contains(&(L4Proto::Udp, 4789, "vxlan")));
403 }
404
405 #[test]
408 fn label_table_new_starts_empty_inheriting_builtin() {
409 let t = LabelTable::new();
410 assert!(t.inherit_builtin());
411 assert_eq!(t.len(), 0);
412 }
413
414 #[test]
415 fn label_table_standalone_does_not_inherit() {
416 let t = LabelTable::standalone();
417 assert!(!t.inherit_builtin());
418 }
419
420 #[test]
421 fn label_table_lookup_uses_override_first() {
422 let mut t = LabelTable::new();
423 t.set(L4Proto::Tcp, 80, "internal-proxy");
424 assert_eq!(t.lookup(L4Proto::Tcp, 80, 33000), Some("internal-proxy"));
426 }
427
428 #[test]
429 fn label_table_lookup_falls_back_to_builtin_when_inherit() {
430 let t = LabelTable::new();
431 assert_eq!(t.lookup(L4Proto::Tcp, 80, 33000), Some("http"));
433 }
434
435 #[test]
436 fn label_table_standalone_returns_none_when_no_override() {
437 let t = LabelTable::standalone();
438 assert_eq!(t.lookup(L4Proto::Tcp, 80, 33000), None);
439 }
440
441 #[test]
442 fn label_table_extend_bulk_sets_entries() {
443 let mut t = LabelTable::new();
444 t.extend([
445 (L4Proto::Tcp, 8765, "grpc-internal"),
446 (L4Proto::Tcp, 9101, "metrics-scrape"),
447 ]);
448 assert_eq!(t.len(), 2);
449 assert_eq!(t.lookup(L4Proto::Tcp, 8765, 0), Some("grpc-internal"));
450 assert_eq!(t.lookup(L4Proto::Tcp, 9101, 0), Some("metrics-scrape"));
451 }
452
453 #[test]
454 fn label_table_set_overrides_existing_label() {
455 let mut t = LabelTable::new();
456 t.set(L4Proto::Tcp, 8765, "old");
457 t.set(L4Proto::Tcp, 8765, "new");
458 assert_eq!(t.lookup(L4Proto::Tcp, 8765, 0), Some("new"));
459 assert_eq!(t.len(), 1);
460 }
461
462 #[test]
463 fn label_table_lookup_tries_src_port_first_then_dst() {
464 let mut t = LabelTable::new();
465 t.set(L4Proto::Tcp, 8765, "src-side");
466 assert_eq!(t.lookup(L4Proto::Tcp, 8765, 9100), Some("src-side"));
468 assert_eq!(t.lookup(L4Proto::Tcp, 33000, 8765), Some("src-side"));
470 }
471
472 #[test]
473 fn label_table_is_send_and_sync() {
474 fn assert_send_sync<T: Send + Sync>() {}
475 assert_send_sync::<LabelTable>();
476 }
477
478 #[test]
481 fn label_table_remove_returns_previous_label() {
482 let mut t = LabelTable::new();
483 t.set(L4Proto::Tcp, 8765, "grpc-internal");
484 assert_eq!(t.remove(L4Proto::Tcp, 8765), Some("grpc-internal"));
485 }
486
487 #[test]
488 fn label_table_remove_absent_returns_none() {
489 let mut t = LabelTable::new();
490 assert_eq!(t.remove(L4Proto::Tcp, 8765), None);
491 }
492
493 #[test]
494 fn label_table_remove_falls_back_to_builtin_when_inherit() {
495 let mut t = LabelTable::new();
496 t.set(L4Proto::Tcp, 80, "internal-proxy");
497 assert_eq!(t.lookup(L4Proto::Tcp, 80, 33000), Some("internal-proxy"));
499 t.remove(L4Proto::Tcp, 80);
500 assert_eq!(t.lookup(L4Proto::Tcp, 80, 33000), Some("http"));
502 }
503
504 #[test]
505 fn label_table_remove_standalone_returns_none_after() {
506 let mut t = LabelTable::standalone();
507 t.set(L4Proto::Tcp, 8765, "grpc-internal");
508 assert_eq!(t.lookup(L4Proto::Tcp, 8765, 0), Some("grpc-internal"));
509 t.remove(L4Proto::Tcp, 8765);
510 assert_eq!(t.lookup(L4Proto::Tcp, 8765, 0), None);
512 }
513
514 #[test]
515 fn label_table_contains_reflects_set_remove() {
516 let mut t = LabelTable::new();
517 assert!(!t.contains(L4Proto::Tcp, 8765));
518 t.set(L4Proto::Tcp, 8765, "grpc-internal");
519 assert!(t.contains(L4Proto::Tcp, 8765));
520 t.remove(L4Proto::Tcp, 8765);
521 assert!(!t.contains(L4Proto::Tcp, 8765));
522 }
523
524 #[test]
525 fn label_table_contains_does_not_consult_builtin() {
526 let t = LabelTable::new();
530 assert!(!t.contains(L4Proto::Tcp, 80));
531 assert_eq!(t.lookup(L4Proto::Tcp, 80, 33000), Some("http"));
533 }
534
535 #[test]
536 fn label_table_is_empty_on_new() {
537 assert!(LabelTable::new().is_empty());
538 assert!(LabelTable::standalone().is_empty());
539 }
540
541 #[test]
542 fn label_table_is_empty_after_set_then_remove() {
543 let mut t = LabelTable::new();
544 t.set(L4Proto::Tcp, 8765, "grpc");
545 assert!(!t.is_empty());
546 t.remove(L4Proto::Tcp, 8765);
547 assert!(t.is_empty());
548 }
549
550 fn key(src_port: u16, dst_port: u16) -> crate::extract::FiveTupleKey {
557 use std::net::{IpAddr, Ipv4Addr, SocketAddr};
558 crate::extract::FiveTupleKey {
559 proto: L4Proto::Tcp,
560 a: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)), src_port),
561 b: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 2)), dst_port),
562 }
563 }
564
565 #[test]
566 fn protocol_label_with_override_wins_over_builtin() {
567 let mut t = LabelTable::new();
568 t.set(L4Proto::Tcp, 80, "internal-proxy");
569 assert_eq!(
571 key(80, 33000).protocol_label_with(&t),
572 Some("internal-proxy"),
573 );
574 }
575
576 #[test]
577 fn protocol_label_with_falls_back_to_builtin_when_inheriting() {
578 let t = LabelTable::new(); assert_eq!(key(80, 33000).protocol_label_with(&t), Some("http"));
580 }
581
582 #[test]
583 fn protocol_label_with_standalone_returns_none_for_unmapped() {
584 let t = LabelTable::standalone(); assert_eq!(key(80, 33000).protocol_label_with(&t), None);
586 }
587
588 #[test]
589 fn app_label_with_falls_back_to_canonical_name() {
590 let t = LabelTable::standalone();
591 assert_eq!(key(33000, 33001).app_label_with(&t), "tcp");
594 }
595
596 #[test]
597 fn app_label_with_override_wins() {
598 let mut t = LabelTable::new();
599 t.set(L4Proto::Tcp, 8765, "grpc-internal");
600 assert_eq!(key(8765, 33000).app_label_with(&t), "grpc-internal");
601 }
602}