1use std::collections::HashMap;
2use std::net::Ipv4Addr;
3use std::path::PathBuf;
4use std::time::Duration;
5
6#[derive(Debug, Clone)]
8pub struct ExposeHostMapping {
9 pub host_port: u16,
11 pub guest_port: u16,
13}
14
15#[derive(Debug, Clone)]
17pub struct ProxyConfig {
18 pub dns_resolver: Ipv4Addr,
20 pub secrets: HashMap<String, SecretConfig>,
24 pub network: NetworkConfig,
26 pub expose_host: Vec<ExposeHostMapping>,
28 pub config_dir: Option<PathBuf>,
32}
33
34impl Default for ProxyConfig {
35 fn default() -> Self {
36 Self {
37 dns_resolver: Ipv4Addr::new(8, 8, 8, 8),
38 secrets: HashMap::new(),
39 network: NetworkConfig::default(),
40 expose_host: Vec::new(),
41 config_dir: None,
42 }
43 }
44}
45
46#[derive(Debug, Clone, Default)]
51pub struct SecretConfig {
52 pub from: Option<String>,
54 pub command: Option<Vec<String>>,
57 pub ttl: Option<Duration>,
59 pub hosts: Vec<String>,
62 pub value: Option<String>,
64}
65
66impl SecretConfig {
67 pub fn from_env(var: impl Into<String>, hosts: Vec<String>) -> Self {
69 Self {
70 from: Some(var.into()),
71 hosts,
72 ..Default::default()
73 }
74 }
75
76 pub fn from_command(command: Vec<String>, hosts: Vec<String>) -> Self {
78 Self {
79 command: Some(command),
80 hosts,
81 ..Default::default()
82 }
83 }
84
85 pub fn matches_domain(&self, domain: &str) -> bool {
87 self.hosts
88 .iter()
89 .any(|pattern| domain_matches(pattern, domain))
90 }
91}
92
93#[derive(Debug, Clone, Default)]
95pub struct NetworkConfig {
96 pub allow: Vec<String>,
99}
100
101impl NetworkConfig {
102 pub fn has_allowlist(&self) -> bool {
104 !self.allow.is_empty()
105 }
106}
107
108impl ProxyConfig {
109 pub fn is_domain_allowed(&self, domain: &str) -> bool {
112 if self.network.allow.is_empty() {
113 return true;
114 }
115 self.network
116 .allow
117 .iter()
118 .any(|pattern| domain_matches(pattern, domain))
119 }
120
121 pub fn exposed_host_port(&self, dst_ip: Ipv4Addr, guest_port: u16) -> Option<u16> {
124 const GATEWAY: Ipv4Addr = Ipv4Addr::new(10, 0, 0, 1);
125 if dst_ip != GATEWAY {
126 return None;
127 }
128 self.expose_host
129 .iter()
130 .find(|m| m.guest_port == guest_port)
131 .map(|m| m.host_port)
132 }
133}
134
135pub(crate) fn domain_matches(pattern: &str, domain: &str) -> bool {
140 if pattern == "*" {
141 true
142 } else if let Some(suffix) = pattern.strip_prefix("*.") {
143 domain.ends_with(suffix)
144 && domain.len() > suffix.len()
145 && domain.as_bytes()[domain.len() - suffix.len() - 1] == b'.'
146 } else {
147 pattern == domain
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154
155 #[test]
156 fn test_exposed_host_port() {
157 use std::net::Ipv4Addr;
158 let config = ProxyConfig {
159 expose_host: vec![
160 ExposeHostMapping {
161 host_port: 3000,
162 guest_port: 8080,
163 },
164 ExposeHostMapping {
165 host_port: 5432,
166 guest_port: 5432,
167 },
168 ],
169 ..Default::default()
170 };
171 assert_eq!(
173 config.exposed_host_port(Ipv4Addr::new(10, 0, 0, 1), 8080),
174 Some(3000)
175 );
176 assert_eq!(
177 config.exposed_host_port(Ipv4Addr::new(10, 0, 0, 1), 5432),
178 Some(5432)
179 );
180 assert_eq!(
182 config.exposed_host_port(Ipv4Addr::new(10, 0, 0, 1), 9999),
183 None
184 );
185 assert_eq!(
187 config.exposed_host_port(Ipv4Addr::new(1, 2, 3, 4), 8080),
188 None
189 );
190 }
191
192 #[test]
193 fn test_domain_matching() {
194 assert!(domain_matches("*", "anything.com"));
195 assert!(domain_matches("*", "api.example.com"));
196 assert!(domain_matches("example.com", "example.com"));
197 assert!(!domain_matches("example.com", "api.example.com"));
198 assert!(domain_matches("*.example.com", "api.example.com"));
199 assert!(domain_matches("*.example.com", "deep.api.example.com"));
200 assert!(!domain_matches("*.example.com", "example.com"));
201 assert!(!domain_matches("*.example.com", "notexample.com"));
202 }
203
204 #[test]
205 fn test_has_allowlist() {
206 let empty = NetworkConfig::default();
207 assert!(!empty.has_allowlist());
208
209 let with_entries = NetworkConfig {
210 allow: vec!["api.example.com".into()],
211 };
212 assert!(with_entries.has_allowlist());
213 }
214
215 #[test]
216 fn default_dns_resolver_is_google_public_dns() {
217 assert_eq!(
218 ProxyConfig::default().dns_resolver,
219 Ipv4Addr::new(8, 8, 8, 8)
220 );
221 }
222}