1use std::fs;
20use std::net::{IpAddr, Ipv4Addr};
21use std::path::{Path, PathBuf};
22
23use ipnet::Ipv4Net;
24use serde::{Deserialize, Serialize};
25use tracing::warn;
26
27#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq, Hash)]
36#[serde(rename_all = "lowercase")]
37pub enum Protocol {
38 Tcp,
39 Udp,
40}
41
42#[derive(Clone, Debug, Deserialize, Serialize)]
45pub struct RawEgressEntry {
46 pub host: String,
47 pub port: u16,
48 pub protocol: Protocol,
49}
50
51pub const SCHEMA_VERSION: u32 = 1;
54
55#[derive(Clone, Debug, Deserialize, Serialize)]
57pub struct RawAllowlist {
58 pub version: u32,
60 #[serde(default, skip_serializing_if = "Vec::is_empty")]
63 pub resolvers: Vec<String>,
64 #[serde(default, skip_serializing_if = "Vec::is_empty")]
66 pub egress: Vec<RawEgressEntry>,
67}
68
69impl RawAllowlist {
70 pub fn new_v1() -> Self {
74 Self {
75 version: SCHEMA_VERSION,
76 resolvers: Vec::new(),
77 egress: Vec::new(),
78 }
79 }
80}
81
82#[derive(Clone, Debug, PartialEq, Eq)]
84pub enum HostMatcher {
85 Literal(Ipv4Addr),
87 Cidr(Ipv4Net),
89}
90
91impl HostMatcher {
92 pub fn contains(&self, ip: Ipv4Addr) -> bool {
93 match self {
94 HostMatcher::Literal(a) => *a == ip,
95 HostMatcher::Cidr(net) => net.contains(&ip),
96 }
97 }
98}
99
100#[derive(Clone, Debug)]
102pub struct AllowlistEntry {
103 pub host: HostMatcher,
104 pub port: u16,
105 pub protocol: Protocol,
106}
107
108#[derive(Clone, Debug, PartialEq, Eq)]
111pub struct HostnameEntry {
112 pub host: String,
115 pub port: u16,
116 pub protocol: Protocol,
117}
118
119#[derive(Clone, Debug, Default)]
121pub struct AllowlistConfig {
122 pub entries: Vec<AllowlistEntry>,
126 pub resolvers: Vec<Ipv4Addr>,
132 pub hostnames: Vec<HostnameEntry>,
136}
137
138#[derive(Debug, thiserror::Error)]
140pub enum AllowlistLoadError {
141 #[error("I/O error reading {0}: {1}")]
142 Io(PathBuf, std::io::Error),
143 #[error("JSON parse error in {0}: {1}")]
144 Json(PathBuf, serde_json::Error),
145 #[error("unsupported allowlist schema version {0} (expected 1)")]
146 UnsupportedVersion(u32),
147 #[error("UDP egress is not supported yet (entry `{host}:{port}/udp`); see https://github.com/EnclaviaIO/enclavia/issues/1")]
148 UdpNotSupported { host: String, port: u16 },
149}
150
151impl AllowlistConfig {
152 pub fn empty() -> Self {
155 Self::default()
156 }
157
158 pub fn load_or_empty(path: &Path) -> Result<Self, AllowlistLoadError> {
162 let bytes = match fs::read(path) {
163 Ok(b) => b,
164 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
165 warn!(path = %path.display(), "Allowlist file missing, defaulting to deny-all");
166 return Ok(Self::empty());
167 }
168 Err(e) => return Err(AllowlistLoadError::Io(path.to_path_buf(), e)),
169 };
170 if bytes.iter().all(|b| b.is_ascii_whitespace()) {
171 warn!(path = %path.display(), "Allowlist file is empty, defaulting to deny-all");
172 return Ok(Self::empty());
173 }
174 Self::from_bytes(&bytes).map_err(|e| match e {
175 AllowlistLoadError::Io(_, ioe) => AllowlistLoadError::Io(path.to_path_buf(), ioe),
176 AllowlistLoadError::Json(_, je) => AllowlistLoadError::Json(path.to_path_buf(), je),
177 other => other,
178 })
179 }
180
181 pub fn from_bytes(bytes: &[u8]) -> Result<Self, AllowlistLoadError> {
184 let raw: RawAllowlist = serde_json::from_slice(bytes)
185 .map_err(|e| AllowlistLoadError::Json(PathBuf::new(), e))?;
186 Self::from_raw(raw)
187 }
188
189 pub fn from_raw(raw: RawAllowlist) -> Result<Self, AllowlistLoadError> {
201 if raw.version != 1 {
202 return Err(AllowlistLoadError::UnsupportedVersion(raw.version));
203 }
204
205 let mut entries = Vec::new();
206 let mut hostnames = Vec::new();
207 for raw_entry in raw.egress {
208 if matches!(raw_entry.protocol, Protocol::Udp) {
213 return Err(AllowlistLoadError::UdpNotSupported {
214 host: raw_entry.host.trim().to_string(),
215 port: raw_entry.port,
216 });
217 }
218 let host = raw_entry.host.trim().to_string();
219 if let Ok(net) = host.parse::<Ipv4Net>() {
220 entries.push(AllowlistEntry {
221 host: HostMatcher::Cidr(net),
222 port: raw_entry.port,
223 protocol: raw_entry.protocol,
224 });
225 continue;
226 }
227 match host.parse::<IpAddr>() {
228 Ok(IpAddr::V4(v4)) => entries.push(AllowlistEntry {
229 host: HostMatcher::Literal(v4),
230 port: raw_entry.port,
231 protocol: raw_entry.protocol,
232 }),
233 Ok(IpAddr::V6(v6)) => {
234 warn!(
235 host = %v6,
236 port = raw_entry.port,
237 protocol = ?raw_entry.protocol,
238 "Ignoring IPv6 allowlist entry: IPv6 egress is always denied",
239 );
240 }
241 Err(_) => {
242 hostnames.push(HostnameEntry {
243 host: host.to_ascii_lowercase(),
244 port: raw_entry.port,
245 protocol: raw_entry.protocol,
246 });
247 }
248 }
249 }
250
251 let mut resolvers = Vec::new();
252 for r in raw.resolvers {
253 match r.trim().parse::<IpAddr>() {
254 Ok(IpAddr::V4(v4)) => resolvers.push(v4),
255 Ok(IpAddr::V6(v6)) => {
256 warn!(resolver = %v6, "Ignoring IPv6 resolver: IPv6 egress is always denied");
257 }
258 Err(_) => {
259 warn!(resolver = %r, "Ignoring non-IPv4 resolver entry");
260 }
261 }
262 }
263
264 Ok(Self {
265 entries,
266 resolvers,
267 hostnames,
268 })
269 }
270
271 pub fn allows_tcp(&self, ip: Ipv4Addr, port: u16) -> bool {
276 self.entries.iter().any(|e| {
277 matches!(e.protocol, Protocol::Tcp) && e.port == port && e.host.contains(ip)
278 })
279 }
280
281 pub fn tcp_hostnames_for_port(&self, port: u16) -> impl Iterator<Item = &HostnameEntry> {
285 self.hostnames
286 .iter()
287 .filter(move |h| matches!(h.protocol, Protocol::Tcp) && h.port == port)
288 }
289
290 pub fn push_entry(&mut self, entry: AllowlistEntry) {
294 self.entries.push(entry);
295 }
296}
297
298#[derive(Debug, thiserror::Error)]
306pub enum AllowlistFlagError {
307 #[error("egress allow spec must be HOST:PORT[/PROTO] (got `{0}`)")]
308 BadEntryShape(String),
309 #[error("egress allow spec `{0}` is missing the :PORT segment")]
310 MissingPort(String),
311 #[error("egress allow spec `{0}` has an invalid port: {1}")]
312 InvalidPort(String, std::num::ParseIntError),
313 #[error("egress allow spec `{0}` has port 0 (must be 1..=65535)")]
314 PortZero(String),
315 #[error("egress allow spec `{0}` has an unsupported protocol `{1}` (expected tcp or udp)")]
316 UnsupportedProtocol(String, String),
317 #[error("egress allow spec `{0}` has an empty host")]
318 EmptyHost(String),
319 #[error("egress allow spec `{0}` uses an IPv6 host; IPv6 egress is always denied")]
320 IpV6Host(String),
321 #[error("egress allow spec `{spec}` has invalid hostname `{host}`: {reason}")]
322 InvalidHostname { spec: String, host: String, reason: &'static str },
323 #[error("resolver spec `{0}` must be an IPv4 address")]
324 InvalidResolver(String),
325 #[error("invalid allowlist: {0}")]
326 Validation(#[from] AllowlistLoadError),
327}
328
329pub fn parse_cli_entry(spec: &str) -> Result<RawEgressEntry, AllowlistFlagError> {
344 let trimmed = spec.trim();
345 if trimmed.is_empty() {
346 return Err(AllowlistFlagError::BadEntryShape(spec.to_string()));
347 }
348
349 let (head, protocol) = match trimmed.rsplit_once('/') {
356 Some((head, tail)) if !tail.contains(':') => {
357 match tail.to_ascii_lowercase().as_str() {
358 "tcp" => (head, Protocol::Tcp),
359 "udp" => (head, Protocol::Udp),
360 _ => {
361 return Err(AllowlistFlagError::UnsupportedProtocol(
362 spec.to_string(),
363 tail.to_string(),
364 ));
365 }
366 }
367 }
368 _ => (trimmed, Protocol::Tcp),
369 };
370
371 let (host, port_str) = head
374 .rsplit_once(':')
375 .ok_or_else(|| AllowlistFlagError::MissingPort(spec.to_string()))?;
376
377 let host = host.trim();
378 if host.is_empty() {
379 return Err(AllowlistFlagError::EmptyHost(spec.to_string()));
380 }
381
382 let port: u16 = port_str
383 .trim()
384 .parse()
385 .map_err(|e| AllowlistFlagError::InvalidPort(spec.to_string(), e))?;
386 if port == 0 {
387 return Err(AllowlistFlagError::PortZero(spec.to_string()));
388 }
389
390 if host.parse::<Ipv4Net>().is_err() && host.parse::<Ipv4Addr>().is_err() {
394 if let Ok(IpAddr::V6(_)) = host.parse::<IpAddr>() {
396 return Err(AllowlistFlagError::IpV6Host(spec.to_string()));
397 }
398 if let Err(reason) = validate_hostname(host) {
399 return Err(AllowlistFlagError::InvalidHostname {
400 spec: spec.to_string(),
401 host: host.to_string(),
402 reason,
403 });
404 }
405 }
406
407 Ok(RawEgressEntry {
408 host: host.to_string(),
409 port,
410 protocol,
411 })
412}
413
414pub fn parse_cli_resolver(spec: &str) -> Result<String, AllowlistFlagError> {
418 let trimmed = spec.trim();
419 match trimmed.parse::<IpAddr>() {
420 Ok(IpAddr::V4(_)) => Ok(trimmed.to_string()),
421 _ => Err(AllowlistFlagError::InvalidResolver(spec.to_string())),
422 }
423}
424
425pub fn assemble_from_cli(
435 allow_specs: &[&str],
436 resolver_specs: &[&str],
437) -> Result<RawAllowlist, AllowlistFlagError> {
438 let mut raw = RawAllowlist::new_v1();
439 for s in allow_specs {
440 raw.egress.push(parse_cli_entry(s)?);
441 }
442 for r in resolver_specs {
443 raw.resolvers.push(parse_cli_resolver(r)?);
444 }
445 AllowlistConfig::from_raw(raw.clone()).map_err(AllowlistFlagError::Validation)?;
450 Ok(raw)
451}
452
453pub fn validate_json(value: &serde_json::Value) -> Result<RawAllowlist, AllowlistFlagError> {
462 let raw: RawAllowlist = serde_json::from_value(value.clone()).map_err(|e| {
463 AllowlistFlagError::Validation(AllowlistLoadError::Json(PathBuf::new(), e))
464 })?;
465 AllowlistConfig::from_raw(raw.clone()).map_err(AllowlistFlagError::Validation)?;
466 Ok(raw)
467}
468
469fn validate_hostname(host: &str) -> Result<(), &'static str> {
476 if host.is_empty() {
477 return Err("empty hostname");
478 }
479 if host.len() > 253 {
480 return Err("hostname exceeds 253 characters");
481 }
482 if host.starts_with('.') || host.ends_with('.') {
483 return Err("hostname must not start or end with a dot");
484 }
485 for label in host.split('.') {
486 if label.is_empty() {
487 return Err("hostname contains an empty label (consecutive dots)");
488 }
489 if label.len() > 63 {
490 return Err("hostname label exceeds 63 characters");
491 }
492 if label.starts_with('-') || label.ends_with('-') {
493 return Err("hostname label must not start or end with a hyphen");
494 }
495 if !label.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') {
496 return Err("hostname label has invalid characters (allowed: a-z, 0-9, '-')");
497 }
498 }
499 Ok(())
500}
501
502#[cfg(test)]
503mod tests {
504 use super::*;
505
506 #[test]
507 fn parses_literal_and_cidr_entries() {
508 let raw = br#"{
509 "version": 1,
510 "resolvers": ["1.1.1.1"],
511 "egress": [
512 {"host": "10.0.0.0/8", "port": 443, "protocol": "tcp"},
513 {"host": "1.2.3.4", "port": 80, "protocol": "tcp"}
514 ]
515 }"#;
516 let cfg = AllowlistConfig::from_bytes(raw).expect("parse");
517 assert_eq!(cfg.entries.len(), 2);
518 assert_eq!(cfg.resolvers, vec![Ipv4Addr::new(1, 1, 1, 1)]);
519 assert!(matches!(cfg.entries[0].host, HostMatcher::Cidr(_)));
520 assert!(matches!(cfg.entries[1].host, HostMatcher::Literal(_)));
521 }
522
523 #[test]
524 fn hostnames_are_recognized_as_first_class() {
525 let raw = br#"{
526 "version": 1,
527 "egress": [
528 {"host": "api.openai.com", "port": 443, "protocol": "tcp"}
529 ]
530 }"#;
531 let cfg = AllowlistConfig::from_bytes(raw).expect("parse");
532 assert!(cfg.entries.is_empty());
533 assert_eq!(cfg.hostnames.len(), 1);
534 assert_eq!(cfg.hostnames[0].host, "api.openai.com");
535 assert_eq!(cfg.hostnames[0].port, 443);
536 assert!(matches!(cfg.hostnames[0].protocol, Protocol::Tcp));
537 }
538
539 #[test]
540 fn hostnames_are_lowercased() {
541 let raw = br#"{
542 "version": 1,
543 "egress": [
544 {"host": "API.Openai.COM", "port": 443, "protocol": "tcp"}
545 ]
546 }"#;
547 let cfg = AllowlistConfig::from_bytes(raw).expect("parse");
548 assert_eq!(cfg.hostnames[0].host, "api.openai.com");
549 }
550
551 #[test]
552 fn ipv6_literal_entries_are_dropped() {
553 let raw = br#"{
554 "version": 1,
555 "egress": [
556 {"host": "::1", "port": 443, "protocol": "tcp"}
557 ]
558 }"#;
559 let cfg = AllowlistConfig::from_bytes(raw).expect("parse");
560 assert!(cfg.entries.is_empty());
561 assert!(cfg.hostnames.is_empty());
562 }
563
564 #[test]
565 fn hostnames_for_port_filters_by_port() {
566 let raw = br#"{
567 "version": 1,
568 "egress": [
569 {"host": "a.example", "port": 443, "protocol": "tcp"},
570 {"host": "b.example", "port": 80, "protocol": "tcp"}
571 ]
572 }"#;
573 let cfg = AllowlistConfig::from_bytes(raw).expect("parse");
574 let on_443: Vec<_> =
575 cfg.tcp_hostnames_for_port(443).map(|h| h.host.as_str()).collect();
576 assert_eq!(on_443, vec!["a.example"]);
577 }
578
579 #[test]
580 fn udp_entry_in_json_is_rejected() {
581 let raw = br#"{
582 "version": 1,
583 "egress": [
584 {"host": "1.1.1.1", "port": 53, "protocol": "udp"}
585 ]
586 }"#;
587 let err = AllowlistConfig::from_bytes(raw).expect_err("must reject UDP");
588 assert!(matches!(
589 err,
590 AllowlistLoadError::UdpNotSupported { ref host, port: 53 } if host == "1.1.1.1"
591 ));
592 }
593
594 #[test]
595 fn udp_entry_in_hostname_form_is_rejected() {
596 let raw = br#"{
597 "version": 1,
598 "egress": [
599 {"host": "example.com", "port": 53, "protocol": "udp"}
600 ]
601 }"#;
602 let err = AllowlistConfig::from_bytes(raw).expect_err("must reject UDP");
603 assert!(matches!(err, AllowlistLoadError::UdpNotSupported { .. }));
604 }
605
606 #[test]
607 fn udp_via_assemble_from_cli_is_rejected() {
608 let err = assemble_from_cli(&["1.1.1.1:53/udp"], &[]).expect_err("must reject UDP");
609 assert!(matches!(
610 err,
611 AllowlistFlagError::Validation(AllowlistLoadError::UdpNotSupported { .. })
612 ));
613 }
614
615 #[test]
616 fn malformed_json_returns_error() {
617 let raw = br#"{ "version": 1, "egress": [ ] "#;
618 let err = AllowlistConfig::from_bytes(raw).expect_err("must fail");
619 assert!(matches!(err, AllowlistLoadError::Json(_, _)));
620 }
621
622 #[test]
623 fn unsupported_version_rejected() {
624 let raw = br#"{ "version": 2, "egress": [] }"#;
625 let err = AllowlistConfig::from_bytes(raw).expect_err("must fail");
626 assert!(matches!(err, AllowlistLoadError::UnsupportedVersion(2)));
627 }
628
629 #[test]
630 fn missing_file_returns_empty() {
631 let path = std::path::PathBuf::from("/tmp/this-path-does-not-exist-egress-XYZ.json");
632 let cfg = AllowlistConfig::load_or_empty(&path).expect("missing file should not error");
633 assert!(cfg.entries.is_empty());
634 }
635
636 #[test]
637 fn empty_file_returns_empty() {
638 let dir = tempfile::tempdir().unwrap();
639 let p = dir.path().join("egress.json");
640 std::fs::write(&p, "\n\n \n").unwrap();
641 let cfg = AllowlistConfig::load_or_empty(&p).expect("load");
642 assert!(cfg.entries.is_empty());
643 }
644
645 #[test]
646 fn duplicate_entries_do_not_panic() {
647 let raw = br#"{
648 "version": 1,
649 "egress": [
650 {"host": "1.2.3.4", "port": 80, "protocol": "tcp"},
651 {"host": "1.2.3.4", "port": 80, "protocol": "tcp"}
652 ]
653 }"#;
654 let cfg = AllowlistConfig::from_bytes(raw).expect("parse");
655 assert_eq!(cfg.entries.len(), 2);
656 assert!(cfg.allows_tcp(Ipv4Addr::new(1, 2, 3, 4), 80));
657 }
658
659 #[test]
660 fn cli_entry_parses_literal_default_tcp() {
661 let e = parse_cli_entry("1.2.3.4:443").unwrap();
662 assert_eq!(e.host, "1.2.3.4");
663 assert_eq!(e.port, 443);
664 assert_eq!(e.protocol, Protocol::Tcp);
665 }
666
667 #[test]
668 fn cli_entry_parses_explicit_tcp() {
669 let e = parse_cli_entry("1.2.3.4:443/tcp").unwrap();
670 assert_eq!(e.protocol, Protocol::Tcp);
671 }
672
673 #[test]
674 fn cli_entry_parses_explicit_udp() {
675 let e = parse_cli_entry("1.2.3.4:53/udp").unwrap();
676 assert_eq!(e.protocol, Protocol::Udp);
677 }
678
679 #[test]
680 fn cli_entry_parses_cidr() {
681 let e = parse_cli_entry("10.0.0.0/8:443").unwrap();
682 assert_eq!(e.host, "10.0.0.0/8");
683 assert_eq!(e.port, 443);
684 }
685
686 #[test]
687 fn cli_entry_parses_cidr_with_proto() {
688 let e = parse_cli_entry("10.0.0.0/8:443/udp").unwrap();
689 assert_eq!(e.host, "10.0.0.0/8");
690 assert_eq!(e.protocol, Protocol::Udp);
691 }
692
693 #[test]
694 fn cli_entry_parses_hostname() {
695 let e = parse_cli_entry("api.example.com:443").unwrap();
696 assert_eq!(e.host, "api.example.com");
697 assert_eq!(e.port, 443);
698 }
699
700 #[test]
701 fn cli_entry_rejects_missing_port() {
702 assert!(matches!(
703 parse_cli_entry("api.example.com"),
704 Err(AllowlistFlagError::MissingPort(_))
705 ));
706 }
707
708 #[test]
709 fn cli_entry_rejects_bad_port() {
710 assert!(matches!(
711 parse_cli_entry("api.example.com:abc"),
712 Err(AllowlistFlagError::InvalidPort(_, _))
713 ));
714 }
715
716 #[test]
717 fn cli_entry_rejects_zero_port() {
718 assert!(matches!(
719 parse_cli_entry("1.2.3.4:0"),
720 Err(AllowlistFlagError::PortZero(_))
721 ));
722 }
723
724 #[test]
725 fn cli_entry_rejects_unknown_protocol() {
726 assert!(matches!(
727 parse_cli_entry("1.2.3.4:443/sctp"),
728 Err(AllowlistFlagError::UnsupportedProtocol(_, _))
729 ));
730 }
731
732 #[test]
733 fn cli_entry_rejects_bad_hostname() {
734 assert!(matches!(
735 parse_cli_entry("foo!bar.com:443"),
736 Err(AllowlistFlagError::InvalidHostname { .. })
737 ));
738 }
739
740 #[test]
741 fn cli_entry_rejects_ipv6() {
742 assert!(matches!(
743 parse_cli_entry("::1:443"),
744 Err(AllowlistFlagError::IpV6Host(_)) | Err(AllowlistFlagError::InvalidHostname { .. })
745 ));
746 }
747
748 #[test]
749 fn cli_resolver_accepts_ipv4() {
750 assert_eq!(parse_cli_resolver("1.1.1.1").unwrap(), "1.1.1.1");
751 }
752
753 #[test]
754 fn cli_resolver_rejects_hostname() {
755 assert!(matches!(
756 parse_cli_resolver("dns.example.com"),
757 Err(AllowlistFlagError::InvalidResolver(_))
758 ));
759 }
760
761 #[test]
762 fn assemble_round_trips_through_from_raw() {
763 let raw = assemble_from_cli(
764 &["10.0.0.0/8:443", "api.example.com:443/tcp", "1.2.3.4:80"],
765 &["1.1.1.1"],
766 )
767 .unwrap();
768 assert_eq!(raw.version, SCHEMA_VERSION);
769 assert_eq!(raw.egress.len(), 3);
770 assert_eq!(raw.resolvers, vec!["1.1.1.1".to_string()]);
771
772 let cfg = AllowlistConfig::from_raw(raw).unwrap();
774 assert_eq!(cfg.entries.len(), 2); assert_eq!(cfg.hostnames.len(), 1);
776 assert_eq!(cfg.resolvers.len(), 1);
777 }
778
779 #[test]
780 fn validate_json_accepts_well_formed_doc() {
781 let v: serde_json::Value = serde_json::from_str(
782 r#"{"version": 1, "resolvers": ["1.1.1.1"], "egress": [{"host":"api.example.com","port":443,"protocol":"tcp"}]}"#,
783 )
784 .unwrap();
785 let raw = validate_json(&v).unwrap();
786 assert_eq!(raw.egress.len(), 1);
787 assert_eq!(raw.resolvers.len(), 1);
788 }
789
790 #[test]
791 fn validate_json_rejects_unknown_version() {
792 let v: serde_json::Value =
793 serde_json::from_str(r#"{"version": 2, "egress": []}"#).unwrap();
794 assert!(matches!(
795 validate_json(&v),
796 Err(AllowlistFlagError::Validation(AllowlistLoadError::UnsupportedVersion(2)))
797 ));
798 }
799
800 #[test]
801 fn quad_zero_slash_zero_matches_everything() {
802 let raw = br#"{
803 "version": 1,
804 "egress": [
805 {"host": "0.0.0.0/0", "port": 19444, "protocol": "tcp"}
806 ]
807 }"#;
808 let cfg = AllowlistConfig::from_bytes(raw).expect("parse");
809 assert!(cfg.allows_tcp(Ipv4Addr::new(1, 2, 3, 4), 19444));
810 assert!(cfg.allows_tcp(Ipv4Addr::new(192, 168, 1, 1), 19444));
811 assert!(!cfg.allows_tcp(Ipv4Addr::new(1, 2, 3, 4), 19445));
812 }
813}