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}