Skip to main content

rust_mc_status/proxy/
socks5.rs

1//! Proxy configuration types for [`McClient`](crate::McClient).
2//!
3//! Supports **SOCKS5** and **HTTP CONNECT** proxies for Java Edition (TCP).
4//!
5//! # Bedrock / UDP note
6//!
7//! Both proxy kinds tunnel TCP only. Bedrock pings (UDP) through a proxy
8//! configured without [`UdpSupport::Yes`] return
9//! [`McError::Proxy(ProxyError::UdpUnsupported)`](crate::error::ProxyError)
10//! immediately. Use [`ProxyConfig::socks5_with_udp`] to opt-in when your
11//! SOCKS5 proxy genuinely supports UDP ASSOCIATE.
12
13use std::fmt;
14
15// ─── ProxyAddr ────────────────────────────────────────────────────────────────
16
17/// Proxy server address, e.g. `"127.0.0.1:1080"` or `"proxy.example.com:3128"`.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct ProxyAddr {
20    pub host: String,
21    pub port: u16,
22}
23
24impl ProxyAddr {
25    pub fn new(host: impl Into<String>, port: u16) -> Self {
26        Self { host: host.into(), port }
27    }
28
29    /// Parse `"host:port"`.
30    pub fn parse(s: &str) -> Result<Self, crate::McError> {
31        match s.rsplit_once(':') {
32            Some((h, p)) => {
33                let port = p.parse::<u16>()
34                    .map_err(|e| crate::McError::invalid_port(e.to_string()))?;
35                Ok(Self::new(h, port))
36            }
37            None => Err(crate::McError::invalid_address(
38                format!("proxy address must be 'host:port', got '{s}'")
39            )),
40        }
41    }
42}
43
44impl fmt::Display for ProxyAddr {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        write!(f, "{}:{}", self.host, self.port)
47    }
48}
49
50// ─── ProxyAuth ────────────────────────────────────────────────────────────────
51
52/// Optional username/password credentials.
53///
54/// The password is never included in `Debug` output or error messages.
55#[derive(Clone)]
56pub struct ProxyAuth {
57    pub username: String,
58    pub password: String,
59}
60
61impl fmt::Debug for ProxyAuth {
62    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63        f.debug_struct("ProxyAuth")
64            .field("username", &self.username)
65            .field("password", &"***")
66            .finish()
67    }
68}
69
70// ─── UdpSupport ───────────────────────────────────────────────────────────────
71
72/// Whether the proxy supports UDP ASSOCIATE (SOCKS5 only, needed for Bedrock).
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
74pub enum UdpSupport {
75    /// No UDP support (default). Bedrock pings return an error immediately.
76    #[default]
77    No,
78    /// Proxy supports UDP ASSOCIATE. Bedrock pings are routed through it.
79    Yes,
80}
81
82// ─── ProxyKind ────────────────────────────────────────────────────────────────
83
84/// Which proxy protocol to use.
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum ProxyKind {
87    /// SOCKS5 proxy — supports both TCP and optionally UDP (via UDP ASSOCIATE).
88    Socks5,
89    /// HTTP CONNECT proxy — TCP tunnel only, no UDP.
90    ///
91    /// Widely supported by corporate firewalls, Squid, Nginx, HAProxy, etc.
92    Http,
93}
94
95// ─── ProxyConfig ─────────────────────────────────────────────────────────────
96
97/// Proxy configuration attached to an [`McClient`](crate::McClient).
98///
99/// # Quick reference
100///
101/// | Constructor | Protocol | Auth | UDP (Bedrock) |
102/// |---|---|---|---|
103/// | [`socks5`](Self::socks5) | SOCKS5 | optional | No |
104/// | [`socks5_with_udp`](Self::socks5_with_udp) | SOCKS5 | optional | Yes |
105/// | [`http`](Self::http) | HTTP CONNECT | optional | No |
106///
107/// # Examples
108///
109/// ```rust
110/// use rust_mc_status::proxy::ProxyConfig;
111///
112/// // SOCKS5 — anonymous
113/// let cfg = ProxyConfig::socks5("127.0.0.1:1080");
114///
115/// // SOCKS5 — authenticated
116/// let cfg = ProxyConfig::socks5("proxy.example.com:1080")
117///     .with_auth("alice", "s3cr3t");
118///
119/// // SOCKS5 — with UDP (Bedrock support)
120/// let cfg = ProxyConfig::socks5_with_udp("10.0.0.1:1080");
121///
122/// // HTTP CONNECT — anonymous
123/// let cfg = ProxyConfig::http("squid.example.com:3128");
124///
125/// // HTTP CONNECT — authenticated
126/// let cfg = ProxyConfig::http("squid.example.com:3128")
127///     .with_auth("user", "pass");
128/// ```
129#[derive(Debug, Clone)]
130pub struct ProxyConfig {
131    pub(crate) addr:        ProxyAddr,
132    pub(crate) auth:        Option<ProxyAuth>,
133    pub(crate) kind:        ProxyKind,
134    pub(crate) udp_support: UdpSupport,
135}
136
137impl ProxyConfig {
138    // ── SOCKS5 ────────────────────────────────────────────────────────────────
139
140    /// SOCKS5 proxy for Java Edition (TCP) only.
141    ///
142    /// Bedrock pings will fail with `ProxyError::UdpUnsupported`.
143    /// Use [`socks5_with_udp`](Self::socks5_with_udp) if your proxy
144    /// supports UDP ASSOCIATE.
145    pub fn socks5(addr: impl AsRef<str>) -> Self {
146        Self {
147            addr:        ProxyAddr::parse(addr.as_ref())
148                             .expect("invalid proxy address — use 'host:port'"),
149            auth:        None,
150            kind:        ProxyKind::Socks5,
151            udp_support: UdpSupport::No,
152        }
153    }
154
155    /// SOCKS5 proxy with UDP ASSOCIATE support (usable for Bedrock too).
156    pub fn socks5_with_udp(addr: impl AsRef<str>) -> Self {
157        Self {
158            addr:        ProxyAddr::parse(addr.as_ref())
159                             .expect("invalid proxy address — use 'host:port'"),
160            auth:        None,
161            kind:        ProxyKind::Socks5,
162            udp_support: UdpSupport::Yes,
163        }
164    }
165
166    // ── HTTP CONNECT ──────────────────────────────────────────────────────────
167
168    /// HTTP CONNECT proxy (TCP tunnel only).
169    ///
170    /// Compatible with Squid, Nginx, HAProxy, corporate HTTP proxies, and any
171    /// server that speaks the `CONNECT` method. UDP is not supported — Bedrock
172    /// pings will fail with `ProxyError::UdpUnsupported`.
173    ///
174    /// Authentication is sent as a `Proxy-Authorization: Basic ...` header.
175    ///
176    /// # Example
177    ///
178    /// ```rust
179    /// use rust_mc_status::proxy::ProxyConfig;
180    ///
181    /// let cfg = ProxyConfig::http("squid.corp.example.com:3128");
182    /// let cfg = ProxyConfig::http("proxy.example.com:8080")
183    ///     .with_auth("user", "pass");
184    /// ```
185    pub fn http(addr: impl AsRef<str>) -> Self {
186        Self {
187            addr:        ProxyAddr::parse(addr.as_ref())
188                             .expect("invalid proxy address — use 'host:port'"),
189            auth:        None,
190            kind:        ProxyKind::Http,
191            udp_support: UdpSupport::No, // HTTP CONNECT is TCP-only
192        }
193    }
194
195    // ── Shared ────────────────────────────────────────────────────────────────
196
197    /// Add username/password authentication.
198    ///
199    /// For SOCKS5 this uses username/password sub-negotiation (RFC 1929).
200    /// For HTTP CONNECT this sends a `Proxy-Authorization: Basic` header.
201    pub fn with_auth(mut self, username: impl Into<String>, password: impl Into<String>) -> Self {
202        self.auth = Some(ProxyAuth {
203            username: username.into(),
204            password: password.into(),
205        });
206        self
207    }
208
209    // ── Accessors ─────────────────────────────────────────────────────────────
210
211    pub fn addr(&self)        -> &ProxyAddr         { &self.addr }
212    pub fn auth(&self)        -> Option<&ProxyAuth>  { self.auth.as_ref() }
213    pub fn kind(&self)        -> ProxyKind           { self.kind }
214    pub fn udp_support(&self) -> UdpSupport          { self.udp_support }
215}