Skip to main content

net/
ssrf.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2//! SSRF classifier (RFC 0012 — security posture, §"SSRF guard").
3//!
4//! A *pure* address classifier plus a DNS-resolving host guard. The
5//! acceptance bar from assessment §4 M6 is blunt: "HTTP client refuses
6//! RFC-1918 / link-local by default". This module is the mechanism; it is
7//! composed at every call site that introduces a model/agent/peer-supplied
8//! URL (A2A push targets, `http` workflow nodes), while the only
9//! operator-configured outbound (`intel/client.rs`) is exempt.
10//!
11//! ## Resolve once, dial what you vetted
12//!
13//! A guard that resolves a name, likes the answer, and then lets the
14//! caller dial the *name* is decorative: the connect re-resolves, and an
15//! attacker who controls the authoritative DNS answers the guard with a
16//! public address and the connect with `169.254.169.254`. That is DNS
17//! rebinding, and it defeats an address check that does not carry its
18//! result forward.
19//!
20//! So the guard hands back the addresses it vetted
21//! ([`resolve_guarded`]) and the dial takes *addresses*, never a name
22//! ([`connect_vetted`] / [`connect_addrs`], which re-assert [`is_global`]
23//! on every address immediately before the syscall). TLS and the `Host`
24//! header stay on the original hostname — connect by IP, verify by name —
25//! so SNI and certificate validation are unaffected.
26//!
27//! [`guard_host`] is retained for the yes/no admission check at
28//! *registration* time, where there is no socket to dial yet; it is not
29//! sufficient on its own at delivery time.
30//!
31//! ## What "non-global" means here
32//!
33//! [`is_global`] returns `false` — i.e. the address is *blocked* — for
34//! any address an attacker could pivot to from inside the appliance's
35//! network namespace:
36//!
37//! * loopback (`127.0.0.0/8`, `::1`)
38//! * RFC-1918 private (`10/8`, `172.16/12`, `192.168/16`)
39//! * link-local (`169.254/16`, `fe80::/10`) — this is the cloud
40//!   metadata range (`169.254.169.254`)
41//! * IPv6 unique-local (`fc00::/7`)
42//! * unspecified (`0.0.0.0`, `::`)
43//! * multicast and the IPv4 limited broadcast (`255.255.255.255`)
44//! * "this network" `0.0.0.0/8` and the IETF/benchmark documentation
45//!   ranges, which never route on the public Internet
46//! * **any IPv4-mapped / IPv4-compatible IPv6** whose embedded v4
47//!   address is itself non-global — `::ffff:127.0.0.1` and friends are
48//!   a classic guard bypass, so we unwrap before classifying.
49//!
50//! We deliberately do NOT lean on `std`'s unstable `IpAddr::is_global`
51//! (feature `ip`, issue #27709) — it is not available on our MSRV and
52//! its semantics drift. Every range below is spelled out by hand from
53//! primitives that are stable on Rust 1.88, in the same
54//! enumerate-the-bytes spirit as the rest of the crate.
55//!
56//! ## Logging posture
57//!
58//! Hosts and IPs are *operational* identifiers, not tool/instruction
59//! content, so the diagnostic carries the host and the offending class
60//! — never request bodies, headers, or secrets. The codebase is
61//! content-capture-off by default and this module keeps that contract.
62
63use std::io;
64use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, TcpStream, ToSocketAddrs};
65use std::time::Duration;
66
67// ---------------------------------------------------------------------------
68// Errors
69// ---------------------------------------------------------------------------
70
71/// A host failed the SSRF guard, or could not be resolved at all.
72///
73/// `Clone`/`Eq` so callers can compare and surface it without owning a
74/// socket; `host` is the operator/tool-supplied authority (not secret),
75/// `reason` is a short human class string (e.g. `"loopback"`).
76#[derive(Debug, Clone, PartialEq, Eq)]
77pub struct SsrfError {
78    /// The host authority that was guarded (no port).
79    pub host: String,
80    /// Short class of the failure, e.g. `"link-local 169.254.169.254"`.
81    pub reason: String,
82}
83
84impl std::fmt::Display for SsrfError {
85    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86        write!(
87            f,
88            "host `{}` rejected by SSRF guard: {}",
89            self.host, self.reason
90        )
91    }
92}
93
94impl std::error::Error for SsrfError {}
95
96fn reject(host: &str, reason: impl Into<String>) -> SsrfError {
97    SsrfError {
98        host: host.to_string(),
99        reason: reason.into(),
100    }
101}
102
103// ---------------------------------------------------------------------------
104// Pure classifier
105// ---------------------------------------------------------------------------
106
107/// `true` iff `ip` is a globally routable unicast address that is safe
108/// to dial from inside the appliance — i.e. *not* in any of the blocked
109/// ranges documented on this module.
110///
111/// Pure: no DNS, no I/O. This is the single source of truth; the host
112/// guard composes it over every resolved address.
113pub fn is_global(ip: IpAddr) -> bool {
114    match ip {
115        IpAddr::V4(v4) => is_global_v4(v4),
116        IpAddr::V6(v6) => is_global_v6(v6),
117    }
118}
119
120/// IPv4 classification. Blocked ranges are spelled out from RFC-3330 /
121/// RFC-1918 / RFC-3927 rather than via `std`'s unstable helpers.
122fn is_global_v4(ip: Ipv4Addr) -> bool {
123    let [a, b, _, _] = ip.octets();
124
125    // "This host on this network" — 0.0.0.0/8 (covers 0.0.0.0).
126    if a == 0 {
127        return false;
128    }
129    // Loopback 127.0.0.0/8, private 10/8 + 172.16/12 + 192.168/16,
130    // link-local 169.254/16, broadcast, multicast 224/4 + reserved
131    // 240/4, all unspecified — std covers these and they are stable.
132    if ip.is_loopback()
133        || ip.is_private()
134        || ip.is_link_local()
135        || ip.is_broadcast()
136        || ip.is_multicast()
137        || ip.is_unspecified()
138        || ip.is_documentation()
139    {
140        return false;
141    }
142    // Carrier-grade NAT (RFC-6598) 100.64.0.0/10 — shared address
143    // space, not globally routable; `is_shared` is unstable so unfold
144    // the prefix by hand.
145    if a == 100 && (64..=127).contains(&b) {
146        return false;
147    }
148    // Reserved 240.0.0.0/4 (minus the broadcast already caught) — never
149    // a routable destination.
150    if a >= 240 {
151        return false;
152    }
153    true
154}
155
156/// IPv6 classification. We first peel IPv4-mapped (`::ffff:0:0/96`) and
157/// IPv4-compatible (`::/96`) forms back to v4 and re-run the v4 rules —
158/// this is the bypass that bites naive guards.
159fn is_global_v6(ip: Ipv6Addr) -> bool {
160    // `::ffff:a.b.c.d` — classify the embedded v4 address.
161    if let Some(v4) = ip.to_ipv4_mapped() {
162        return is_global_v4(v4);
163    }
164    // `::a.b.c.d` (deprecated IPv4-compatible, plus ::1 / ::). `to_ipv4`
165    // also yields the mapped form, but the mapped case is handled
166    // above; here it catches the compatible range. ::1 and :: classify
167    // as loopback/unspecified v4-side too, but we also guard them
168    // directly below for clarity.
169    if let Some(v4) = ip.to_ipv4() {
170        return is_global_v4(v4);
171    }
172
173    if ip.is_loopback() || ip.is_unspecified() || ip.is_multicast() {
174        return false;
175    }
176
177    let segments = ip.segments();
178    // Link-local unicast fe80::/10 (top 10 bits == 1111 1110 10).
179    if (segments[0] & 0xffc0) == 0xfe80 {
180        return false;
181    }
182    // Unique-local fc00::/7 (top 7 bits == 1111 110x).
183    if (segments[0] & 0xfe00) == 0xfc00 {
184        return false;
185    }
186    // Documentation 2001:db8::/32 — never globally routable.
187    if segments[0] == 0x2001 && segments[1] == 0x0db8 {
188        return false;
189    }
190    true
191}
192
193// ---------------------------------------------------------------------------
194// Host guard (DNS-resolving)
195// ---------------------------------------------------------------------------
196
197/// Resolve `host` and reject if *any* resolved address is non-global.
198///
199/// This is the deny-all-the-aliases stance: a hostname that resolves to
200/// both a public and a private address is rejected, because an attacker
201/// who controls DNS could otherwise race the second connect (a DNS
202/// rebinding pivot).
203///
204/// **This answers a yes/no question and throws the addresses away**, so
205/// it is only sound where there is nothing to dial yet — admission of a
206/// push target at registration, config validation. Anything that goes on
207/// to open a socket MUST use [`resolve_guarded`] + [`connect_addrs`] (or
208/// [`connect_vetted`], which does both), or the connect re-resolves and
209/// the check it just passed means nothing.
210///
211/// `allow_private == true` is the operator escape hatch — it skips the
212/// check entirely without even resolving, so trusted localhost/private
213/// gateways (the configured intelligence endpoint) keep working. Callers
214/// that take a MODEL/AGENT-supplied URL MUST pass `false`.
215///
216/// Pure-ish: the only side effect is DNS resolution. No bytes are sent.
217pub fn guard_host(host: &str, allow_private: bool) -> Result<(), SsrfError> {
218    if allow_private {
219        return Ok(());
220    }
221    // Port 0 because we are only classifying: the resolver needs a port
222    // grammar and we discard the addresses anyway.
223    resolve_guarded(host, 0, false).map(|_| ())
224}
225
226// ---------------------------------------------------------------------------
227// Resolver seam
228// ---------------------------------------------------------------------------
229
230/// How a host is turned into addresses. A plain `fn` pointer, not a
231/// trait object: the only production implementation is [`std_resolve`],
232/// and the seam exists so a test can install a *hostile* resolver that
233/// answers the guard and the dial differently — the rebinding shape this
234/// module has to survive.
235pub type ResolveFn = fn(&str, u16) -> io::Result<Vec<SocketAddr>>;
236
237/// The production resolver: `std`'s `ToSocketAddrs`, with the bracketed
238/// IPv6 literal form (`[::1]`, as URLs write it) unwrapped first because
239/// `ToSocketAddrs` does not accept the brackets on a bare host.
240pub fn std_resolve(host: &str, port: u16) -> io::Result<Vec<SocketAddr>> {
241    let bare = host
242        .strip_prefix('[')
243        .and_then(|s| s.strip_suffix(']'))
244        .unwrap_or(host);
245    // An IP literal short-circuits DNS entirely — no syscall, and no
246    // opportunity for a resolver to answer with something else.
247    if let Ok(ip) = bare.parse::<IpAddr>() {
248        return Ok(vec![SocketAddr::new(ip, port)]);
249    }
250    (host, port).to_socket_addrs().map(|it| it.collect())
251}
252
253// ---------------------------------------------------------------------------
254// Resolve-once guard + dial-what-you-vetted
255// ---------------------------------------------------------------------------
256
257/// Resolve `host:port` **once** and return the addresses, having rejected
258/// the whole host if *any* of them is non-global.
259///
260/// The returned vector is the only thing a caller may dial: passing the
261/// name to a second resolution is precisely the rebinding hole this
262/// exists to close.
263///
264/// `allow_private` still resolves (there has to be something to connect
265/// to) but skips the classification, matching [`guard_host`]'s escape
266/// hatch.
267pub fn resolve_guarded(
268    host: &str,
269    port: u16,
270    allow_private: bool,
271) -> Result<Vec<SocketAddr>, SsrfError> {
272    resolve_guarded_with(host, port, allow_private, std_resolve)
273}
274
275/// [`resolve_guarded`] against an injected resolver. Public so the
276/// rebinding regression test can drive both halves — guard and dial —
277/// through a resolver that changes its mind between them.
278pub fn resolve_guarded_with(
279    host: &str,
280    port: u16,
281    allow_private: bool,
282    resolve: ResolveFn,
283) -> Result<Vec<SocketAddr>, SsrfError> {
284    if host.is_empty() {
285        return Err(reject(host, "empty host"));
286    }
287    let addrs = resolve(host, port).map_err(|e| reject(host, format!("resolve failed: {e}")))?;
288    if addrs.is_empty() {
289        return Err(reject(host, "no addresses resolved"));
290    }
291    if !allow_private {
292        for sa in &addrs {
293            check_addr(host, sa.ip())?;
294        }
295    }
296    Ok(addrs)
297}
298
299/// Dial one of `addrs`, re-asserting the classifier on every entry first.
300///
301/// The re-check is not redundant paranoia: this is the last instruction
302/// before the syscall, so it is the only place that can promise the bytes
303/// go somewhere global. A caller that hands over an address list built
304/// any other way (a cached answer, a redirect target) gets the same
305/// refusal, and one non-global entry refuses the *whole* dial rather than
306/// falling through to the next address — the same deny-all-the-aliases
307/// stance [`resolve_guarded`] takes, so a mixed answer cannot be raced.
308///
309/// `host` is carried for diagnostics only. TLS/SNI and the `Host` header
310/// remain the caller's business and must stay on the original hostname.
311pub fn connect_addrs(
312    host: &str,
313    addrs: &[SocketAddr],
314    timeout: Duration,
315    allow_private: bool,
316) -> io::Result<TcpStream> {
317    if addrs.is_empty() {
318        return Err(io::Error::new(
319            io::ErrorKind::NotFound,
320            format!("no vetted addresses for {host}"),
321        ));
322    }
323    if !allow_private {
324        for sa in addrs {
325            if let Err(e) = check_addr(host, sa.ip()) {
326                return Err(io::Error::new(
327                    io::ErrorKind::PermissionDenied,
328                    e.to_string(),
329                ));
330            }
331        }
332    }
333    // Every address was vetted above, so trying the next one on a
334    // connect failure cannot widen the target set — it is only
335    // dual-stack fallback.
336    let mut last: Option<io::Error> = None;
337    for sa in addrs {
338        match TcpStream::connect_timeout(sa, timeout) {
339            Ok(stream) => {
340                stream.set_read_timeout(Some(timeout))?;
341                stream.set_write_timeout(Some(timeout))?;
342                stream.set_nodelay(true).ok();
343                return Ok(stream);
344            }
345            Err(e) => last = Some(e),
346        }
347    }
348    Err(last.unwrap_or_else(|| {
349        io::Error::new(io::ErrorKind::NotFound, format!("cannot connect to {host}"))
350    }))
351}
352
353/// Guard and dial in one step: resolve once, vet, connect to a vetted
354/// address. This is what a model/peer-supplied URL must use instead of
355/// `http::connect_tcp`, which resolves the name a second time.
356pub fn connect_vetted(
357    host: &str,
358    port: u16,
359    timeout: Duration,
360    allow_private: bool,
361) -> io::Result<TcpStream> {
362    connect_vetted_with(host, port, timeout, allow_private, std_resolve)
363}
364
365/// [`connect_vetted`] against an injected resolver — the test seam.
366pub fn connect_vetted_with(
367    host: &str,
368    port: u16,
369    timeout: Duration,
370    allow_private: bool,
371    resolve: ResolveFn,
372) -> io::Result<TcpStream> {
373    let addrs = resolve_guarded_with(host, port, allow_private, resolve)
374        .map_err(|e| io::Error::new(io::ErrorKind::PermissionDenied, e.to_string()))?;
375    connect_addrs(host, &addrs, timeout, allow_private)
376}
377
378/// Classify one resolved address, turning a non-global result into a
379/// typed rejection with a short class string.
380fn check_addr(host: &str, ip: IpAddr) -> Result<(), SsrfError> {
381    if is_global(ip) {
382        Ok(())
383    } else {
384        Err(reject(host, format!("{} ({ip})", class_of(ip))))
385    }
386}
387
388/// Best-effort human label for *why* an address is non-global. Purely
389/// cosmetic — `is_global` remains the authority on the boolean.
390fn class_of(ip: IpAddr) -> &'static str {
391    match ip {
392        IpAddr::V4(v4) => {
393            if v4.is_unspecified() {
394                "unspecified"
395            } else if v4.is_loopback() {
396                "loopback"
397            } else if v4.is_private() {
398                "private (RFC-1918)"
399            } else if v4.is_link_local() {
400                "link-local"
401            } else if v4.is_broadcast() {
402                "broadcast"
403            } else if v4.is_multicast() {
404                "multicast"
405            } else {
406                "reserved"
407            }
408        }
409        IpAddr::V6(v6) => {
410            if let Some(v4) = v6.to_ipv4_mapped().or_else(|| v6.to_ipv4()) {
411                return class_of(IpAddr::V4(v4));
412            }
413            if v6.is_unspecified() {
414                "unspecified"
415            } else if v6.is_loopback() {
416                "loopback"
417            } else if v6.is_multicast() {
418                "multicast"
419            } else {
420                "link-local/unique-local"
421            }
422        }
423    }
424}
425
426// ---------------------------------------------------------------------------
427// Tests — LITERAL IPs only, never DNS.
428// ---------------------------------------------------------------------------
429
430#[cfg(test)]
431mod tests {
432    use super::*;
433
434    fn v4(a: u8, b: u8, c: u8, d: u8) -> IpAddr {
435        IpAddr::V4(Ipv4Addr::new(a, b, c, d))
436    }
437
438    fn v6(s: &str) -> IpAddr {
439        IpAddr::V6(s.parse::<Ipv6Addr>().expect("test ipv6 literal"))
440    }
441
442    #[test]
443    fn public_v4_is_global() {
444        assert!(is_global(v4(8, 8, 8, 8)));
445        assert!(is_global(v4(1, 1, 1, 1)));
446        assert!(is_global(v4(93, 184, 216, 34))); // example.com historic
447        assert!(is_global(v4(172, 15, 255, 255))); // just below 172.16/12
448        assert!(is_global(v4(172, 32, 0, 1))); // just above 172.31
449        assert!(is_global(v4(11, 0, 0, 1))); // just above 10/8
450        assert!(is_global(v4(192, 167, 255, 255))); // just below 192.168/16
451        assert!(is_global(v4(192, 169, 0, 1))); // just above 192.168/16
452        assert!(is_global(v4(100, 63, 255, 255))); // just below CGNAT 100.64/10
453        assert!(is_global(v4(100, 128, 0, 1))); // just above CGNAT
454    }
455
456    #[test]
457    fn loopback_blocked() {
458        assert!(!is_global(v4(127, 0, 0, 1)));
459        assert!(!is_global(v4(127, 255, 255, 255)));
460        assert!(!is_global(v6("::1")));
461    }
462
463    #[test]
464    fn rfc1918_blocked() {
465        // 10/8
466        assert!(!is_global(v4(10, 0, 0, 0)));
467        assert!(!is_global(v4(10, 255, 255, 255)));
468        // 172.16/12
469        assert!(!is_global(v4(172, 16, 0, 0)));
470        assert!(!is_global(v4(172, 16, 0, 1)));
471        assert!(!is_global(v4(172, 31, 255, 255)));
472        // 192.168/16
473        assert!(!is_global(v4(192, 168, 0, 1)));
474        assert!(!is_global(v4(192, 168, 255, 255)));
475    }
476
477    #[test]
478    fn link_local_and_metadata_blocked() {
479        assert!(!is_global(v4(169, 254, 0, 1)));
480        // The cloud metadata endpoint — the whole point of M6.
481        assert!(!is_global(v4(169, 254, 169, 254)));
482        assert!(!is_global(v4(169, 254, 255, 255)));
483        // IPv6 link-local fe80::/10 — both ends of the prefix.
484        assert!(!is_global(v6("fe80::1")));
485        assert!(!is_global(v6("febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff")));
486    }
487
488    #[test]
489    fn unique_local_blocked() {
490        // fc00::/7 covers fc00:: and fd00::.
491        assert!(!is_global(v6("fc00::1")));
492        assert!(!is_global(v6("fd12:3456:789a::1")));
493        assert!(!is_global(v6("fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")));
494    }
495
496    #[test]
497    fn unspecified_blocked() {
498        assert!(!is_global(v4(0, 0, 0, 0)));
499        assert!(!is_global(v4(0, 1, 2, 3))); // 0/8 "this network"
500        assert!(!is_global(v6("::")));
501    }
502
503    #[test]
504    fn multicast_and_broadcast_blocked() {
505        assert!(!is_global(v4(224, 0, 0, 1)));
506        assert!(!is_global(v4(239, 255, 255, 255)));
507        assert!(!is_global(v4(255, 255, 255, 255))); // limited broadcast
508        assert!(!is_global(v6("ff02::1")));
509    }
510
511    #[test]
512    fn reserved_v4_blocked() {
513        assert!(!is_global(v4(240, 0, 0, 1)));
514        assert!(!is_global(v4(255, 0, 0, 1)));
515    }
516
517    #[test]
518    fn ipv4_mapped_bypass_is_caught() {
519        // ::ffff:127.0.0.1 must classify as loopback, not as a global
520        // v6 address. This is the headline bypass.
521        assert!(!is_global(v6("::ffff:127.0.0.1")));
522        assert!(!is_global(v6("::ffff:10.0.0.1")));
523        assert!(!is_global(v6("::ffff:169.254.169.254")));
524        assert!(!is_global(v6("::ffff:192.168.1.1")));
525        // A mapped *public* v4 stays global.
526        assert!(is_global(v6("::ffff:8.8.8.8")));
527    }
528
529    #[test]
530    fn ipv4_compatible_bypass_is_caught() {
531        // ::a.b.c.d (deprecated) — embedded private v4 must be blocked.
532        assert!(!is_global(v6("::10.0.0.1")));
533        assert!(!is_global(v6("::169.254.169.254")));
534    }
535
536    #[test]
537    fn public_v6_is_global() {
538        assert!(is_global(v6("2606:4700:4700::1111"))); // 1.1.1.1 v6
539        assert!(is_global(v6("2001:4860:4860::8888"))); // google dns v6
540    }
541
542    #[test]
543    fn ipv6_documentation_blocked() {
544        assert!(!is_global(v6("2001:db8::1")));
545    }
546
547    // --- guard_host over literals (no DNS) ---
548
549    #[test]
550    fn guard_rejects_ip_literals() {
551        assert!(guard_host("127.0.0.1", false).is_err());
552        assert!(guard_host("10.0.0.5", false).is_err());
553        assert!(guard_host("169.254.169.254", false).is_err());
554        assert!(guard_host("::1", false).is_err());
555        assert!(guard_host("[::1]", false).is_err()); // bracketed
556        assert!(guard_host("[fe80::1]", false).is_err());
557        assert!(guard_host("::ffff:127.0.0.1", false).is_err());
558    }
559
560    #[test]
561    fn guard_allows_public_ip_literals() {
562        assert!(guard_host("8.8.8.8", false).is_ok());
563        assert!(guard_host("1.1.1.1", false).is_ok());
564        assert!(guard_host("[2606:4700:4700::1111]", false).is_ok());
565    }
566
567    #[test]
568    fn allow_private_skips_everything() {
569        // The operator escape hatch — must not even fail on a literal
570        // private address, since the intel endpoint is often localhost.
571        assert!(guard_host("127.0.0.1", true).is_ok());
572        assert!(guard_host("10.0.0.5", true).is_ok());
573        assert!(guard_host("", true).is_ok());
574        assert!(guard_host("anything.invalid", true).is_ok());
575    }
576
577    #[test]
578    fn empty_host_rejected_when_guarded() {
579        assert!(guard_host("", false).is_err());
580    }
581
582    #[test]
583    fn error_carries_host_and_class() {
584        let err = guard_host("169.254.169.254", false).unwrap_err();
585        assert_eq!(err.host, "169.254.169.254");
586        assert!(err.reason.contains("link-local"), "reason: {}", err.reason);
587        // Display must surface both without panicking.
588        let shown = err.to_string();
589        assert!(shown.contains("169.254.169.254"));
590        assert!(shown.contains("SSRF guard"));
591    }
592
593    #[test]
594    fn class_labels_are_specific() {
595        assert_eq!(class_of(v4(127, 0, 0, 1)), "loopback");
596        assert_eq!(class_of(v4(10, 0, 0, 1)), "private (RFC-1918)");
597        assert_eq!(class_of(v4(169, 254, 1, 1)), "link-local");
598        assert_eq!(class_of(v4(0, 0, 0, 0)), "unspecified");
599        assert_eq!(class_of(v4(224, 0, 0, 1)), "multicast");
600        assert_eq!(class_of(v4(255, 255, 255, 255)), "broadcast");
601        assert_eq!(class_of(v4(240, 0, 0, 1)), "reserved");
602        // mapped v6 borrows the v4 label.
603        assert_eq!(class_of(v6("::ffff:10.0.0.1")), "private (RFC-1918)");
604    }
605}