amadeus_utils/
ip_resolver.rs

1use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
2use std::time::Duration;
3use tokio::{net, time};
4
5const STUN_MAGIC_COOKIE: u32 = 0x2112A442;
6
7/// Resolves current public IPv4 address
8/// Resolution order:
9/// 1) STUN (stun.l.google.com:19302, 6s timeout)
10/// 2) HTTP (http://api.myip.la/en?json, 6s timeout)
11pub async fn resolve_public_ipv4() -> Option<Ipv4Addr> {
12    // STUN
13    if let Some(ip) = get_ip_stun().await.ok().flatten() {
14        return Some(ip);
15    }
16
17    // HTTP as string then parse
18    match get_ip_http().await {
19        Ok(Some(ip_str)) => ip_str.parse().ok(),
20        _ => None,
21    }
22}
23
24/// Same as resolve_public_ipv4 but returns String
25pub async fn resolve_public_ipv4_string() -> Option<String> {
26    resolve_public_ipv4().await.map(|ip| ip.to_string())
27}
28
29fn build_stun_binding_request(txid: &[u8; 12]) -> [u8; 20] {
30    let mut buf = [0u8; 20];
31    // type: Binding Request (0x0001)
32    buf[0] = 0x00;
33    buf[1] = 0x01;
34    // length: 0
35    buf[2] = 0x00;
36    buf[3] = 0x00;
37    // magic cookie 0x2112A442
38    buf[4..8].copy_from_slice(&STUN_MAGIC_COOKIE.to_be_bytes());
39    // transaction id
40    buf[8..20].copy_from_slice(txid);
41    buf
42}
43
44/// Public for testing/diagnostics
45pub fn parse_xor_mapped_v4(resp: &[u8], _txid: &[u8; 12]) -> Option<Ipv4Addr> {
46    if resp.len() < 20 {
47        return None;
48    }
49    let msg_len = u16::from_be_bytes([resp[2], resp[3]]) as usize;
50    if resp.len() < 20 + msg_len {
51        return None;
52    }
53    let mut offset = 20;
54    while offset + 4 <= 20 + msg_len {
55        let atype = u16::from_be_bytes([resp[offset], resp[offset + 1]]);
56        let alen = u16::from_be_bytes([resp[offset + 2], resp[offset + 3]]) as usize;
57        offset += 4;
58        if offset + alen > resp.len() {
59            return None;
60        }
61        if atype == 0x0020 {
62            // XOR-MAPPED-ADDRESS
63            if alen < 8 {
64                return None;
65            }
66            let family = resp[offset + 1];
67            if family != 0x01 {
68                return None;
69            }
70            let xport = u16::from_be_bytes([resp[offset + 2], resp[offset + 3]]);
71            let _port = xport ^ ((STUN_MAGIC_COOKIE >> 16) as u16);
72            let mut xaddr = [0u8; 4];
73            xaddr.copy_from_slice(&resp[offset + 4..offset + 8]);
74            let addr_be = u32::from_be_bytes(xaddr) ^ STUN_MAGIC_COOKIE;
75            let octets = addr_be.to_be_bytes();
76            return Some(Ipv4Addr::new(octets[0], octets[1], octets[2], octets[3]));
77        }
78        let pad = (4 - (alen % 4)) % 4;
79        offset += alen + pad;
80    }
81    None
82}
83
84async fn get_ip_stun() -> Result<Option<Ipv4Addr>, std::io::Error> {
85    use rand::RngCore;
86    let mut txid = [0u8; 12];
87    rand::rng().fill_bytes(&mut txid);
88    let req = build_stun_binding_request(&txid);
89
90    let local = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0));
91    let socket = net::UdpSocket::bind(local).await?;
92
93    // resolve stun.l.google.com:19302
94    let addrs: Vec<SocketAddr> = net::lookup_host(("stun.l.google.com", 19302)).await?.collect();
95    if let Some(target) = addrs.iter().find(|sa| sa.is_ipv4()).cloned().or_else(|| addrs.get(0).cloned()) {
96        let _ = socket.send_to(&req, target).await?;
97        let mut buf = [0u8; 1500];
98        match time::timeout(Duration::from_millis(6000), socket.recv_from(&mut buf)).await {
99            Ok(Ok((n, _addr))) => Ok(parse_xor_mapped_v4(&buf[..n], &txid)),
100            _ => Ok(None),
101        }
102    } else {
103        Ok(None)
104    }
105}
106
107async fn get_ip_http() -> Result<Option<String>, std::io::Error> {
108    use tokio::io::{AsyncReadExt, AsyncWriteExt};
109    let host = "api.myip.la";
110    let port = 80;
111    let req = b"GET /en?json HTTP/1.1\r\nHost: api.myip.la\r\nConnection: close\r\n\r\n";
112
113    let stream = match time::timeout(Duration::from_millis(6000), net::TcpStream::connect((host, port))).await {
114        Ok(Ok(s)) => s,
115        _ => return Ok(None),
116    };
117
118    let mut stream = stream;
119    if time::timeout(Duration::from_millis(6000), stream.write_all(req)).await.is_err() {
120        return Ok(None);
121    }
122
123    let mut buf = Vec::with_capacity(4096);
124    match time::timeout(Duration::from_millis(6000), async {
125        let mut tmp = [0u8; 2048];
126        loop {
127            let n = stream.read(&mut tmp).await?;
128            if n == 0 {
129                break;
130            }
131            buf.extend_from_slice(&tmp[..n]);
132        }
133        Ok::<(), std::io::Error>(())
134    })
135    .await
136    {
137        Ok(Ok(())) => {}
138        _ => return Ok(None),
139    }
140
141    // Split headers and body
142    let mut body = &buf[..];
143    if let Some(pos) = buf.windows(4).position(|w| w == b"\r\n\r\n") {
144        body = &buf[pos + 4..];
145    }
146
147    // Try to handle simple chunked transfer encoding
148    let is_chunked = {
149        let headers = &buf[..buf.len().saturating_sub(body.len() + 4)];
150        let headers_str = String::from_utf8_lossy(headers).to_ascii_lowercase();
151        headers_str.contains("transfer-encoding: chunked")
152    };
153
154    let body_bytes = if is_chunked {
155        match decode_chunked(body) {
156            Some(b) => b,
157            None => body.to_vec(),
158        }
159    } else {
160        body.to_vec()
161    };
162
163    if let Ok(v) = serde_json::from_slice::<serde_json::Value>(&body_bytes) {
164        if let Some(ip) = v.get("ip").and_then(|x| x.as_str()) {
165            return Ok(Some(ip.to_string()));
166        }
167    }
168    Ok(None)
169}
170
171fn decode_chunked(mut body: &[u8]) -> Option<Vec<u8>> {
172    let mut out = Vec::new();
173    loop {
174        // read size line
175        let pos = body.windows(2).position(|w| w == b"\r\n")?;
176        let size_str = std::str::from_utf8(&body[..pos]).ok()?.trim();
177        let size = usize::from_str_radix(size_str.trim_end_matches(|c: char| c.is_ascii_whitespace()), 16).ok()?;
178        body = &body[pos + 2..];
179        if size == 0 {
180            break;
181        }
182        if body.len() < size + 2 {
183            return None;
184        }
185        out.extend_from_slice(&body[..size]);
186        body = &body[size + 2..]; // skip CRLF
187    }
188    Some(out)
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    #[test]
196    fn xor_mapped_v4_parsing() {
197        // Construct a fake response with XOR-MAPPED-ADDRESS
198        let txid = [1u8; 12];
199        let mut resp = Vec::new();
200        // header
201        resp.extend_from_slice(&[0x01, 0x01, 0x00, 0x0c]); // success response, 12 bytes attrs
202        resp.extend_from_slice(&STUN_MAGIC_COOKIE.to_be_bytes());
203        resp.extend_from_slice(&txid);
204        // attribute header: type 0x0020, len 8
205        resp.extend_from_slice(&[0x00, 0x20, 0x00, 0x08]);
206        // value: 0, family=1, xport, xaddr
207        let port: u16 = 54321;
208        let xport = port ^ ((STUN_MAGIC_COOKIE >> 16) as u16);
209        resp.extend_from_slice(&[0x00, 0x01]);
210        resp.extend_from_slice(&xport.to_be_bytes());
211        let ip = Ipv4Addr::new(8, 8, 8, 8);
212        let xaddr = u32::from_be_bytes(ip.octets()) ^ STUN_MAGIC_COOKIE;
213        resp.extend_from_slice(&xaddr.to_be_bytes());
214
215        let parsed = parse_xor_mapped_v4(&resp, &txid).unwrap();
216        assert_eq!(parsed, ip);
217    }
218}