Skip to main content

eggress_server/accept/
mod.rs

1//! Inbound accept: authentication, detection, and protocol handshakes.
2//!
3//! Entry points (`accept`, `accept_with_fixed_target[_for_peer]`)
4//! live here with auth/session types; protocol-specific handlers
5//! (`handlers`), HTTP-forward parsing (`forward`), detection
6//! (`detect`), and the prefixed stream (`prefixed`) are submodules.
7
8use std::collections::HashMap;
9use std::fmt;
10use std::net::IpAddr;
11use std::sync::atomic::{AtomicU64, Ordering};
12use std::sync::{Arc, Mutex, MutexGuard};
13use std::time::{Duration, Instant};
14
15static AUTH_CACHE_EPOCH: std::sync::LazyLock<Instant> = std::sync::LazyLock::new(Instant::now);
16
17use eggress_core::BoxStream;
18use eggress_core::{ClientIdentity, ProtocolId, TargetAddr};
19use tokio::io::AsyncReadExt;
20use zeroize::Zeroize;
21
22pub(crate) mod detect;
23pub(crate) mod forward;
24pub(crate) mod handlers;
25pub(crate) mod prefixed;
26#[cfg(test)]
27mod tests;
28
29pub(crate) use detect::{detect_http_method, DetectResult};
30#[cfg(test)]
31pub(crate) use forward::read_http_head;
32pub(crate) use handlers::{accept_http, accept_socks4, accept_socks5};
33pub(crate) use prefixed::PrefixedStream;
34
35/// Authentication policy for inbound connections.
36/// Bounded compatibility authentication state keyed by source IP.
37///
38/// This intentionally lives in the server crate but is only constructed by
39/// the pproxy compatibility runtime. Native Eggress listeners continue to
40/// authenticate every connection independently.
41pub struct AuthReuseCache {
42    timeout: Duration,
43    entries: Mutex<HashMap<IpAddr, AuthReuseEntry>>,
44    max_entries: usize,
45    /// Nanosecond timestamp of the last full expiration sweep. Used to bound
46    /// how often `record` may run an O(n) `retain` under the cache lock;
47    /// expired entries are otherwise evicted lazily on `lookup`.
48    last_sweep_nanos: AtomicU64,
49}
50
51struct AuthReuseEntry {
52    identity: ClientIdentity,
53    last_authenticated: Instant,
54}
55
56impl AuthReuseCache {
57    pub const DEFAULT_MAX_ENTRIES: usize = 4096;
58
59    /// Minimum gap between full expiration sweeps inside `record`. Lookup-time
60    /// expiration still runs on every call, so a long gap only delays the
61    /// reclaim of entries that are never looked up again.
62    const SWEEP_INTERVAL: Duration = Duration::from_secs(30);
63
64    pub fn new(timeout: Duration) -> Self {
65        Self {
66            timeout,
67            entries: Mutex::new(HashMap::new()),
68            max_entries: Self::DEFAULT_MAX_ENTRIES,
69            last_sweep_nanos: AtomicU64::new(0),
70        }
71    }
72
73    fn lock_entries(&self) -> MutexGuard<'_, HashMap<IpAddr, AuthReuseEntry>> {
74        self.entries.lock().unwrap_or_else(|error| {
75            tracing::warn!("auth reuse cache was poisoned; clearing it: {error}");
76            let mut entries = error.into_inner();
77            entries.clear();
78            self.entries.clear_poison();
79            entries
80        })
81    }
82
83    pub fn lookup(&self, peer_ip: IpAddr) -> Option<ClientIdentity> {
84        let mut entries = self.lock_entries();
85        let entry = entries.get(&peer_ip)?;
86        if Instant::now().duration_since(entry.last_authenticated) > self.timeout {
87            entries.remove(&peer_ip);
88            return None;
89        }
90        Some(entry.identity.clone())
91    }
92
93    pub fn record(&self, peer_ip: IpAddr, identity: ClientIdentity) {
94        let mut entries = self.lock_entries();
95        let now = Instant::now();
96        // Expired entries are removed lazily by `lookup`; only pay for a full
97        // sweep once the cache is actually at capacity *and* a full
98        // sweep has not run in the last SWEEP_INTERVAL. This caps the
99        // tail-latency hit of an O(n) `retain` under the cache lock. The
100        // interval check is evaluated while the lock is held so the sweep
101        // schedule is time-based rather than lock-contention dependent.
102        if entries.len() >= self.max_entries {
103            let now_nanos = u64::try_from(
104                now.checked_duration_since(*AUTH_CACHE_EPOCH)
105                    .unwrap_or(Duration::ZERO)
106                    .as_nanos(),
107            )
108            .unwrap_or(u64::MAX);
109            let last_sweep = self.last_sweep_nanos.load(Ordering::Acquire);
110            let interval_nanos = u64::try_from(Self::SWEEP_INTERVAL.as_nanos()).unwrap_or(u64::MAX);
111            if now_nanos.saturating_sub(last_sweep) >= interval_nanos {
112                self.last_sweep_nanos.store(now_nanos, Ordering::Release);
113                entries.retain(|_, entry| {
114                    now.duration_since(entry.last_authenticated) <= self.timeout
115                });
116            }
117        }
118        if entries.len() >= self.max_entries && !entries.contains_key(&peer_ip) {
119            if let Some(oldest) = entries
120                .iter()
121                .min_by_key(|(_, entry)| entry.last_authenticated)
122                .map(|(ip, _)| *ip)
123            {
124                entries.remove(&oldest);
125            }
126        }
127        entries.insert(
128            peer_ip,
129            AuthReuseEntry {
130                identity,
131                last_authenticated: now,
132            },
133        );
134    }
135
136    pub fn len(&self) -> usize {
137        self.lock_entries().len()
138    }
139
140    pub fn is_empty(&self) -> bool {
141        self.len() == 0
142    }
143}
144
145#[derive(Clone)]
146pub enum InboundAuthentication {
147    None,
148    UsernamePassword {
149        username: String,
150        password: String,
151    },
152    UsernamePasswordWithReuse {
153        username: String,
154        password: String,
155        reuse: Arc<AuthReuseCache>,
156    },
157}
158
159impl Drop for InboundAuthentication {
160    fn drop(&mut self) {
161        match self {
162            Self::None => {}
163            Self::UsernamePassword { password, .. }
164            | Self::UsernamePasswordWithReuse { password, .. } => password.zeroize(),
165        }
166    }
167}
168
169impl fmt::Debug for InboundAuthentication {
170    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171        match self {
172            InboundAuthentication::None => write!(f, "InboundAuthentication::None"),
173            InboundAuthentication::UsernamePassword { .. } => {
174                write!(f, "InboundAuthentication::UsernamePassword {{ .. }}")
175            }
176            InboundAuthentication::UsernamePasswordWithReuse { .. } => write!(
177                f,
178                "InboundAuthentication::UsernamePasswordWithReuse {{ .. }}"
179            ),
180        }
181    }
182}
183
184impl fmt::Display for InboundAuthentication {
185    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186        match self {
187            InboundAuthentication::None => write!(f, "none"),
188            InboundAuthentication::UsernamePassword { .. } => write!(f, "username/password"),
189            InboundAuthentication::UsernamePasswordWithReuse { .. } => {
190                write!(f, "username/password with IP reuse")
191            }
192        }
193    }
194}
195
196pub(crate) fn auth_credentials(
197    auth: &InboundAuthentication,
198) -> Option<(&str, &str, Option<&AuthReuseCache>)> {
199    match auth {
200        InboundAuthentication::None => None,
201        InboundAuthentication::UsernamePassword { username, password } => {
202            Some((username, password, None))
203        }
204        InboundAuthentication::UsernamePasswordWithReuse {
205            username,
206            password,
207            reuse,
208        } => Some((username, password, Some(reuse))),
209    }
210}
211
212pub(crate) fn cached_identity(
213    auth: &InboundAuthentication,
214    peer_ip: Option<IpAddr>,
215) -> Option<ClientIdentity> {
216    let (_, _, reuse) = auth_credentials(auth)?;
217    peer_ip.and_then(|ip| reuse.and_then(|cache| cache.lookup(ip)))
218}
219
220pub(crate) fn record_authenticated(
221    auth: &InboundAuthentication,
222    peer_ip: Option<IpAddr>,
223    identity: &ClientIdentity,
224) {
225    let Some((_, _, Some(cache))) = auth_credentials(auth) else {
226        return;
227    };
228    if let Some(ip) = peer_ip {
229        cache.record(ip, identity.clone());
230    }
231}
232
233/// Error type for accept operations.
234#[derive(Debug, thiserror::Error)]
235
236pub enum AcceptError {
237    #[error("protocol error")]
238    Protocol(#[source] Box<dyn std::error::Error + Send + Sync>),
239    #[error("authentication failed")]
240    AuthenticationFailed,
241}
242
243impl From<Box<dyn std::error::Error + Send + Sync>> for AcceptError {
244    fn from(e: Box<dyn std::error::Error + Send + Sync>) -> Self {
245        AcceptError::Protocol(e)
246    }
247}
248
249/// The result of accepting an inbound connection.
250pub enum AcceptedSession {
251    Tunnel(PendingTunnel),
252    HttpForward(PendingHttpForward),
253    UdpAssociate(PendingUdpAssociate),
254    Echo(BoxStream),
255}
256
257/// A pending tunnel connection (HTTP CONNECT, SOCKS4, SOCKS5).
258/// Success reply has NOT been sent yet.
259pub struct PendingTunnel {
260    pub target: TargetAddr,
261    pub client: BoxStream,
262    pub protocol: TunnelProtocol,
263    pub reply_context: ReplyContext,
264    pub identity: ClientIdentity,
265}
266
267/// A pending HTTP forward-proxy request.
268pub struct PendingHttpForward {
269    pub target: TargetAddr,
270    pub client: BoxStream,
271    pub request: eggress_protocol_http::forward::ForwardRequest,
272    pub identity: ClientIdentity,
273}
274
275/// A pending SOCKS5 UDP ASSOCIATE session.
276pub struct PendingUdpAssociate {
277    pub client: BoxStream,
278    pub protocol: TunnelProtocol,
279    pub identity: ClientIdentity,
280    pub client_hint: Option<TargetAddr>,
281}
282
283/// Which tunnel protocol was used.
284#[derive(Debug, Clone, Copy, PartialEq, Eq)]
285pub enum TunnelProtocol {
286    HttpConnect,
287    Http2,
288    Http3,
289    WebSocket,
290    Socks4,
291    Socks5,
292    Shadowsocks,
293    ShadowsocksR,
294    Trojan,
295    Raw,
296}
297
298/// Information needed to send a protocol-specific reply later.
299pub enum ReplyContext {
300    Http,
301    Http2,
302    Http3,
303    WebSocket,
304    Socks4,
305    Socks5,
306    Shadowsocks,
307    Trojan,
308    Raw,
309}
310
311/// Configuration for Shadowsocks inbound listener.
312#[derive(Clone)]
313pub struct InboundShadowsocksConfig {
314    pub method: String,
315    pub password: String,
316    #[cfg(feature = "pproxy-legacy")]
317    pub auth_prefix: Option<Vec<u8>>,
318    #[cfg(feature = "pproxy-legacy")]
319    pub plugins: Vec<String>,
320}
321
322impl Drop for InboundShadowsocksConfig {
323    fn drop(&mut self) {
324        self.password.zeroize();
325    }
326}
327
328impl fmt::Debug for InboundShadowsocksConfig {
329    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
330        let mut ds = f.debug_struct("InboundShadowsocksConfig");
331        ds.field("method", &self.method);
332        ds.field("password", &"***");
333        #[cfg(feature = "pproxy-legacy")]
334        {
335            ds.field("auth_prefix", &self.auth_prefix);
336            ds.field("plugins", &self.plugins);
337        }
338        ds.finish()
339    }
340}
341
342/// Configuration for Trojan inbound listener.
343#[derive(Clone)]
344pub struct InboundTrojanConfig {
345    pub password: String,
346    /// Optional fallback target for auth-failed connections.
347    /// When set, connections with invalid Trojan passwords are relayed to this
348    /// target instead of being rejected (matches pproxy's chaining behavior).
349    pub fallback: Option<String>,
350}
351
352impl Drop for InboundTrojanConfig {
353    fn drop(&mut self) {
354        self.password.zeroize();
355    }
356}
357
358impl fmt::Debug for InboundTrojanConfig {
359    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
360        f.debug_struct("InboundTrojanConfig")
361            .field("password", &"***")
362            .field("fallback", &self.fallback)
363            .finish()
364    }
365}
366
367/// Read the first byte from the stream, detect the protocol, perform the
368/// handshake parsing, and return an `AcceptedSession` **without** opening
369/// any outbound connection or sending any success/failure reply.
370pub async fn accept(
371    client: BoxStream,
372    protocols: &[ProtocolId],
373    auth: &InboundAuthentication,
374    shadowsocks_config: Option<&InboundShadowsocksConfig>,
375    #[cfg(feature = "extended")] shadowsocks_metrics: Option<
376        &std::sync::Arc<eggress_protocol_shadowsocks::ShadowsocksMetrics>,
377    >,
378    #[cfg(not(feature = "extended"))] shadowsocks_metrics: Option<&()>,
379    trojan_config: Option<&InboundTrojanConfig>,
380) -> Result<AcceptedSession, AcceptError> {
381    #[cfg(not(feature = "extended"))]
382    let _ = (shadowsocks_config, shadowsocks_metrics, trojan_config);
383    accept_with_fixed_target(
384        client,
385        protocols,
386        auth,
387        shadowsocks_config,
388        shadowsocks_metrics,
389        trojan_config,
390        None,
391    )
392    .await
393}
394
395pub async fn accept_with_fixed_target(
396    client: BoxStream,
397    protocols: &[ProtocolId],
398    auth: &InboundAuthentication,
399    shadowsocks_config: Option<&InboundShadowsocksConfig>,
400    #[cfg(feature = "extended")] shadowsocks_metrics: Option<
401        &std::sync::Arc<eggress_protocol_shadowsocks::ShadowsocksMetrics>,
402    >,
403    #[cfg(not(feature = "extended"))] shadowsocks_metrics: Option<&()>,
404    trojan_config: Option<&InboundTrojanConfig>,
405    fixed_target: Option<&TargetAddr>,
406) -> Result<AcceptedSession, AcceptError> {
407    accept_with_fixed_target_for_peer(
408        client,
409        protocols,
410        auth,
411        shadowsocks_config,
412        shadowsocks_metrics,
413        trojan_config,
414        fixed_target,
415        None,
416    )
417    .await
418}
419
420#[allow(clippy::too_many_arguments)]
421pub async fn accept_with_fixed_target_for_peer(
422    client: BoxStream,
423    protocols: &[ProtocolId],
424    auth: &InboundAuthentication,
425    shadowsocks_config: Option<&InboundShadowsocksConfig>,
426    #[cfg(feature = "extended")] shadowsocks_metrics: Option<
427        &std::sync::Arc<eggress_protocol_shadowsocks::ShadowsocksMetrics>,
428    >,
429    #[cfg(not(feature = "extended"))] shadowsocks_metrics: Option<&()>,
430    trojan_config: Option<&InboundTrojanConfig>,
431    fixed_target: Option<&TargetAddr>,
432    peer_ip: Option<IpAddr>,
433) -> Result<AcceptedSession, AcceptError> {
434    #[cfg(not(feature = "extended"))]
435    let _ = (shadowsocks_config, shadowsocks_metrics, trojan_config);
436    #[cfg(feature = "extended")]
437    #[inline]
438    fn shadows_metrics(
439        m: Option<&std::sync::Arc<eggress_protocol_shadowsocks::ShadowsocksMetrics>>,
440    ) -> Option<std::sync::Arc<eggress_protocol_shadowsocks::ShadowsocksMetrics>> {
441        m.cloned()
442    }
443    let mut stream = client;
444    if protocols.len() == 1 && protocols.contains(&ProtocolId::Echo) {
445        return Ok(AcceptedSession::Echo(stream));
446    }
447    if protocols.len() == 1 && protocols.contains(&ProtocolId::Raw) {
448        let target = fixed_target
449            .cloned()
450            .ok_or_else(|| AcceptError::Protocol("raw listener requires fixed_target".into()))?;
451        return Ok(AcceptedSession::Tunnel(PendingTunnel {
452            target,
453            client: stream,
454            protocol: TunnelProtocol::Raw,
455            reply_context: ReplyContext::Raw,
456            identity: ClientIdentity::Anonymous,
457        }));
458    }
459    let mut first_byte = [0u8; 1];
460    stream
461        .read_exact(&mut first_byte)
462        .await
463        .map_err(|e| AcceptError::Protocol(Box::new(e)))?;
464
465    let has_socks5 = protocols.contains(&ProtocolId::Socks5);
466    let has_socks4 = protocols.contains(&ProtocolId::Socks4);
467    let has_http = protocols.contains(&ProtocolId::Http);
468
469    // Check SOCKS5
470    if first_byte[0] == 0x05 && has_socks5 {
471        tracing::trace!(
472            "detected protocol: socks5 (first_byte={:#04x})",
473            first_byte[0]
474        );
475        let stream: BoxStream = Box::new(PrefixedStream::new(first_byte.to_vec(), stream));
476        return accept_socks5(stream, auth, peer_ip).await;
477    }
478
479    // Check SOCKS4
480    if first_byte[0] == 0x04 && has_socks4 {
481        tracing::trace!(
482            "detected protocol: socks4 (first_byte={:#04x})",
483            first_byte[0]
484        );
485        let stream: BoxStream = Box::new(PrefixedStream::new(first_byte.to_vec(), stream));
486        return accept_socks4(stream, auth, peer_ip).await;
487    }
488
489    // Try HTTP detection if HTTP is allowed
490    if has_http {
491        // Read more bytes to detect the HTTP method
492        let mut prefix = vec![first_byte[0]];
493        let mut buf = [0u8; 32];
494        let n = stream
495            .read(&mut buf)
496            .await
497            .map_err(|e| AcceptError::Protocol(Box::new(e)))?;
498        prefix.extend_from_slice(&buf[..n]);
499
500        match detect_http_method(&prefix) {
501            DetectResult::Match => {
502                tracing::trace!(
503                    "detected protocol: http (prefix={:?})",
504                    &prefix[..prefix.len().min(16)]
505                );
506                let stream: BoxStream = Box::new(PrefixedStream::new(prefix, stream));
507                return accept_http(stream, auth, peer_ip).await;
508            }
509            DetectResult::NeedMore => {
510                // Read more bytes
511                let mut more = [0u8; 32];
512                let n = stream
513                    .read(&mut more)
514                    .await
515                    .map_err(|e| AcceptError::Protocol(Box::new(e)))?;
516                prefix.extend_from_slice(&more[..n]);
517                match detect_http_method(&prefix) {
518                    DetectResult::Match => {
519                        tracing::trace!(
520                            "detected protocol: http (prefix={:?})",
521                            &prefix[..prefix.len().min(16)]
522                        );
523                        let stream: BoxStream = Box::new(PrefixedStream::new(prefix, stream));
524                        return accept_http(stream, auth, peer_ip).await;
525                    }
526                    DetectResult::NoMatch => {
527                        return Err(AcceptError::Protocol(
528                            "no matching protocol for listener".into(),
529                        ));
530                    }
531                    DetectResult::NeedMore => {
532                        return Err(AcceptError::Protocol(
533                            "no matching protocol for listener".into(),
534                        ));
535                    }
536                }
537            }
538            DetectResult::NoMatch => {
539                return Err(AcceptError::Protocol(
540                    "no matching protocol for listener".into(),
541                ));
542            }
543        }
544    }
545
546    // Check if Shadowsocks is the only protocol (auto-detection not possible)
547    #[cfg(feature = "extended")]
548    if protocols.len() == 1 && protocols.contains(&ProtocolId::Shadowsocks) {
549        if let Some(ss_config) = shadowsocks_config {
550            let stream: BoxStream = Box::new(PrefixedStream::new(first_byte.to_vec(), stream));
551            match eggress_protocol_shadowsocks::CipherMethod::parse_method(&ss_config.method) {
552                Ok(method) => {
553                    let (ss_stream, target_addr) =
554                        eggress_protocol_shadowsocks::tcp::shadowsocks_accept(
555                            stream,
556                            &ss_config.password,
557                            method,
558                            shadows_metrics(shadowsocks_metrics),
559                        )
560                        .await
561                        .map_err(|e| AcceptError::Protocol(Box::new(e)))?;
562
563                    return Ok(AcceptedSession::Tunnel(PendingTunnel {
564                        target: target_addr,
565                        client: ss_stream,
566                        protocol: TunnelProtocol::Shadowsocks,
567                        reply_context: ReplyContext::Shadowsocks,
568                        identity: ClientIdentity::Anonymous,
569                    }));
570                }
571                Err(modern_error) => {
572                    #[cfg(feature = "legacy-crypto")]
573                    if let Ok(legacy_method) =
574                        eggress_protocol_shadowsocks::legacy::LegacyMethod::parse(&ss_config.method)
575                    {
576                        let (ss_stream, target_addr) =
577                            eggress_protocol_shadowsocks::legacy::legacy_accept(
578                                stream,
579                                legacy_method,
580                                ss_config.password.as_bytes(),
581                            )
582                            .await
583                            .map_err(|e| AcceptError::Protocol(Box::new(e)))?;
584
585                        return Ok(AcceptedSession::Tunnel(PendingTunnel {
586                            target: target_addr,
587                            client: ss_stream,
588                            protocol: TunnelProtocol::Shadowsocks,
589                            reply_context: ReplyContext::Shadowsocks,
590                            identity: ClientIdentity::Anonymous,
591                        }));
592                    }
593                    if let Some(m) = shadowsocks_metrics {
594                        m.record_tcp_unsupported_method_reject();
595                    }
596                    return Err(AcceptError::Protocol(Box::new(modern_error)));
597                }
598            }
599        }
600        return Err(AcceptError::Protocol(
601            "shadowsocks listener requires shadowsocks config".into(),
602        ));
603    }
604
605    #[cfg(feature = "pproxy-legacy")]
606    if protocols.len() == 1 && protocols.contains(&ProtocolId::ShadowsocksR) {
607        let ssr_config = shadowsocks_config
608            .filter(|config| config.method == "ssr")
609            .ok_or_else(|| AcceptError::Protocol("SSR listener requires SSR config".into()))?;
610        let plugins =
611            eggress_protocol_shadowsocks::compat::plugin::parse_plugins(&ssr_config.plugins)
612                .map_err(|e| AcceptError::Protocol(Box::new(e)))?;
613        let stream: BoxStream = Box::new(PrefixedStream::new(first_byte.to_vec(), stream));
614        let (ss_stream, target_addr) = eggress_protocol_shadowsocks::compat::ssr::ssr_accept(
615            stream,
616            &eggress_protocol_shadowsocks::compat::ssr::SsrConfig {
617                auth_prefix: ssr_config.auth_prefix.clone(),
618                plugins,
619            },
620        )
621        .await
622        .map_err(|e| AcceptError::Protocol(Box::new(e)))?;
623        return Ok(AcceptedSession::Tunnel(PendingTunnel {
624            target: target_addr,
625            client: ss_stream,
626            protocol: TunnelProtocol::ShadowsocksR,
627            reply_context: ReplyContext::Shadowsocks,
628            identity: ClientIdentity::Anonymous,
629        }));
630    }
631    #[cfg(not(feature = "pproxy-legacy"))]
632    if protocols.len() == 1 && protocols.contains(&ProtocolId::ShadowsocksR) {
633        return Err(AcceptError::Protocol(
634            "SSR compatibility support is not included in this build".into(),
635        ));
636    }
637    #[cfg(not(feature = "extended"))]
638    if protocols.len() == 1 && protocols.contains(&ProtocolId::Shadowsocks) {
639        return Err(AcceptError::Protocol(
640            "shadowsocks support not included in this build".into(),
641        ));
642    }
643
644    // Check if Trojan is the only protocol (TLS termination already happened upstream)
645    #[cfg(feature = "extended")]
646    if protocols.len() == 1 && protocols.contains(&ProtocolId::Trojan) {
647        if let Some(trojan_cfg) = trojan_config {
648            use tokio::io::AsyncReadExt;
649
650            // Read the 56-byte hash prefix to check password before consuming
651            // the rest of the handshake. This enables fallback routing on auth
652            // failure without consuming bytes needed by the fallback target.
653            let mut hash_prefix = [0u8; 56];
654            // The protocol detector already consumed the first hash byte.
655            // Preserve it so password verification and the full Trojan parser
656            // see the original 56-byte hash.
657            hash_prefix[0] = first_byte[0];
658            stream
659                .read_exact(&mut hash_prefix[1..])
660                .await
661                .map_err(|e| AcceptError::Protocol(Box::new(e)))?;
662
663            let password_matches =
664                eggress_protocol_trojan::trojan_check_password(&hash_prefix, &trojan_cfg.password);
665
666            if password_matches {
667                // Replay the 56-byte hash so trojan_accept reads the full handshake
668                let prefixed = PrefixedStream::new(hash_prefix.to_vec(), stream);
669                let boxed: BoxStream = Box::new(prefixed);
670                let (trojan_stream, result) =
671                    eggress_protocol_trojan::trojan_accept(boxed, &trojan_cfg.password)
672                        .await
673                        .map_err(|e| AcceptError::Protocol(Box::new(e)))?;
674
675                return Ok(AcceptedSession::Tunnel(PendingTunnel {
676                    target: result.target,
677                    client: trojan_stream,
678                    protocol: TunnelProtocol::Trojan,
679                    reply_context: ReplyContext::Trojan,
680                    identity: ClientIdentity::Anonymous,
681                }));
682            }
683
684            // Password did not match — check for fallback routing
685            if let Some(ref fallback_target) = trojan_cfg.fallback {
686                // The password hash and its CRLF delimiter belong to the
687                // rejected Trojan handshake, not to the fallback protocol.
688                // Consume the delimiter and let the fallback see only the
689                // application bytes that follow it.
690                let mut delimiter = [0u8; 2];
691                stream
692                    .read_exact(&mut delimiter)
693                    .await
694                    .map_err(|e| AcceptError::Protocol(Box::new(e)))?;
695                if delimiter != *b"\r\n" {
696                    tracing::warn!(?delimiter, "trojan fallback delimiter was not CRLF");
697                }
698                let target: TargetAddr = fallback_target.parse().map_err(|e: String| {
699                    AcceptError::Protocol(format!("invalid trojan fallback address: {e}").into())
700                })?;
701                tracing::debug!("trojan auth failed, falling back to {}", fallback_target);
702                return Ok(AcceptedSession::Tunnel(PendingTunnel {
703                    target,
704                    client: stream,
705                    protocol: TunnelProtocol::Trojan,
706                    reply_context: ReplyContext::Trojan,
707                    identity: ClientIdentity::Anonymous,
708                }));
709            }
710
711            return Err(AcceptError::AuthenticationFailed);
712        }
713        return Err(AcceptError::Protocol(
714            "trojan listener requires trojan config".into(),
715        ));
716    }
717    #[cfg(not(feature = "extended"))]
718    if protocols.len() == 1 && protocols.contains(&ProtocolId::Trojan) {
719        return Err(AcceptError::Protocol(
720            "trojan support not included in this build".into(),
721        ));
722    }
723
724    Err(AcceptError::Protocol(
725        "no matching protocol for listener".into(),
726    ))
727}