eggress_runtime/supervisor/
reload.rs1#[derive(Debug)]
12pub enum ReloadResult {
13 Applied { generation: u64, upstreams: usize },
15 Rejected { reason: String },
17 Failed { error: String },
19}
20
21pub(crate) fn classify_listeners(
52 old_listeners: &[eggress_config::compile::ListenerConfig],
53 new_listeners: &[eggress_config::compile::ListenerConfig],
54) -> Result<(), String> {
55 if old_listeners.len() != new_listeners.len() {
56 return Err(format!(
57 "listener count changed ({} -> {}); restart required",
58 old_listeners.len(),
59 new_listeners.len()
60 ));
61 }
62
63 for (old, new) in old_listeners.iter().zip(new_listeners.iter()) {
64 if old.name != new.name {
65 return Err(format!(
66 "listener name changed ('{}' -> '{}'); restart required",
67 old.name, new.name
68 ));
69 }
70 if old.bind != new.bind {
71 return Err(format!(
72 "listener bind address changed for '{}': '{}' -> '{}'; restart required",
73 old.name, old.bind, new.bind
74 ));
75 }
76 match (&old.udp, &new.udp) {
77 (Some(old_udp), Some(new_udp)) => {
78 if old_udp.bind != new_udp.bind
85 || old_udp.enabled != new_udp.enabled
86 || old_udp.mode != new_udp.mode
87 || old_udp.upstream_udp_bind != new_udp.upstream_udp_bind
88 {
89 return Err(format!(
90 "UDP listener configuration changed for '{}'; restart required",
91 old.name
92 ));
93 }
94 if old_udp.max_associations != new_udp.max_associations
96 || old_udp.max_associations_global != new_udp.max_associations_global
97 || old_udp.max_targets_per_association != new_udp.max_targets_per_association
98 || old_udp.max_datagram_size != new_udp.max_datagram_size
99 {
100 return Err(format!(
101 "UDP listener limits changed for '{}'; restart required",
102 old.name
103 ));
104 }
105 if old_udp.idle_timeout != new_udp.idle_timeout
107 || old_udp.target_idle_timeout != new_udp.target_idle_timeout
108 || old_udp.upstream_connect_timeout != new_udp.upstream_connect_timeout
109 || old_udp.client_pin != new_udp.client_pin
110 || old_udp.allow_private_egress != new_udp.allow_private_egress
111 || old_udp.advertise != new_udp.advertise
112 || old_udp.fixed_target != new_udp.fixed_target
113 {
114 return Err(format!(
115 "UDP listener settings changed for '{}'; restart required",
116 old.name
117 ));
118 }
119 }
120 (None, Some(new_udp)) => {
121 return Err(format!(
122 "UDP configuration added for '{}': '{}'; restart required",
123 new.name, new_udp.bind
124 ));
125 }
126 (Some(_old_udp), None) => {
127 return Err(format!(
128 "UDP configuration removed for '{}'; restart required",
129 old.name
130 ));
131 }
132 (None, None) => {}
133 }
134
135 match (&old.transparent, &new.transparent) {
136 (Some(old_t), Some(new_t)) => {
137 if old_t.enabled != new_t.enabled || old_t.protocol != new_t.protocol {
140 return Err(format!(
141 "transparent config changed for '{}': enabled {} -> {}; restart required",
142 old.name, old_t.enabled, new_t.enabled
143 ));
144 }
145 }
146 (None, Some(new_t)) => {
147 if new_t.enabled {
148 return Err(format!(
149 "transparent proxy enabled for '{}'; restart required",
150 new.name
151 ));
152 }
153 }
154 (Some(old_t), None) if old_t.enabled => {
155 return Err(format!(
156 "transparent proxy configuration removed for '{}'; restart required",
157 old.name
158 ));
159 }
160 (Some(_old_t), None) => {}
161 (None, None) => {}
162 }
163
164 match (&old.unix, &new.unix) {
165 (Some(old_u), Some(new_u)) => {
166 if old_u.path != new_u.path
169 || old_u.unlink_existing != new_u.unlink_existing
170 || old_u.mode != new_u.mode
171 {
172 return Err(format!(
173 "unix socket path changed for '{}': '{}' -> '{}'; restart required",
174 old.name,
175 old_u.path.display(),
176 new_u.path.display()
177 ));
178 }
179 }
180 (None, Some(_new_u)) => {
181 return Err(format!(
182 "unix socket added for '{}'; restart required",
183 new.name
184 ));
185 }
186 (Some(_old_u), None) => {
187 return Err(format!(
188 "unix socket removed for '{}'; restart required",
189 old.name
190 ));
191 }
192 (None, None) => {}
193 }
194
195 if old.reuse_port != new.reuse_port {
202 return Err(format!(
203 "listener socket options changed for '{}'; restart required",
204 old.name
205 ));
206 }
207 if old.protocols != new.protocols {
209 return Err(format!(
210 "listener protocols changed for '{}'; restart required",
211 old.name
212 ));
213 }
214 match (&old.auth, &new.auth) {
217 (None, None) => {}
218 (Some(_), None) | (None, Some(_)) => {
219 return Err(format!(
220 "listener auth presence changed for '{}'; restart required",
221 old.name
222 ));
223 }
224 (Some(old_a), Some(new_a)) => {
225 if old_a.auth_type != new_a.auth_type
226 || old_a.username != new_a.username
227 || old_a.password != new_a.password
228 || old_a.password_env != new_a.password_env
229 {
230 return Err(format!(
231 "listener auth material changed for '{}'; restart required",
232 old.name
233 ));
234 }
235 }
236 }
237 match (&old.tls, &new.tls) {
240 (None, None) => {}
241 (Some(_), None) | (None, Some(_)) => {
242 return Err(format!(
243 "listener TLS presence changed for '{}'; restart required",
244 old.name
245 ));
246 }
247 (Some(old_t), Some(new_t)) => {
248 if old_t.cert_pem != new_t.cert_pem
249 || old_t.key_pem != new_t.key_pem
250 || old_t.alpn != new_t.alpn
251 {
252 return Err(format!(
253 "listener TLS material changed for '{}'; restart required",
254 old.name
255 ));
256 }
257 }
258 }
259 match (&old.shadowsocks, &new.shadowsocks) {
261 (None, None) => {}
262 (Some(_), None) | (None, Some(_)) => {
263 return Err(format!(
264 "listener shadowsocks presence changed for '{}'; restart required",
265 old.name
266 ));
267 }
268 (Some(old_s), Some(new_s)) => {
269 if old_s.method != new_s.method
270 || old_s.password != new_s.password
271 || old_s.auth_prefix != new_s.auth_prefix
272 || old_s.plugins != new_s.plugins
273 {
274 return Err(format!(
275 "listener shadowsocks configuration changed for '{}'; restart required",
276 old.name
277 ));
278 }
279 }
280 }
281 match (&old.trojan, &new.trojan) {
282 (None, None) => {}
283 (Some(_), None) | (None, Some(_)) => {
284 return Err(format!(
285 "listener trojan presence changed for '{}'; restart required",
286 old.name
287 ));
288 }
289 (Some(old_t), Some(new_t)) => {
290 if old_t.password != new_t.password || old_t.fallback != new_t.fallback {
291 return Err(format!(
292 "listener trojan configuration changed for '{}'; restart required",
293 old.name
294 ));
295 }
296 }
297 }
298 if old.connection_limit != new.connection_limit
300 || old.fixed_target != new.fixed_target
301 || old.local_bind != new.local_bind
302 {
303 return Err(format!(
304 "listener connection_limit/fixed_target/local_bind changed for '{}'; restart required",
305 old.name
306 ));
307 }
308 }
309
310 Ok(())
311}
312
313pub fn classify_reload_config(
314 old_listeners: &[eggress_config::compile::ListenerConfig],
315 old_timeouts: &eggress_config::compile::TimeoutConfig,
316 old_admin: Option<&eggress_config::compile::AdminConfig>,
317 new_config: &eggress_config::compile::RuntimeConfig,
318) -> Result<(), String> {
319 classify_listeners(old_listeners, &new_config.listeners)?;
320 if old_timeouts != &new_config.timeouts {
321 return Err("timeout configuration changed; restart required".to_string());
322 }
323
324 let old_admin_endpoint = old_admin.map(|admin| (admin.enabled, admin.bind.as_str()));
325 let new_admin_endpoint = new_config
326 .admin
327 .as_ref()
328 .map(|admin| (admin.enabled, admin.bind.as_str()));
329 if old_admin_endpoint != new_admin_endpoint {
330 return Err("admin endpoint bind configuration changed; restart required".to_string());
331 }
332
333 Ok(())
334}