review 001 remediation: the forked core's findings resolved
Batch 1 (F-1 + N-1 + S-2 + N-4): - reply_success/reply_error write replies with write_all (partial-write hazard closed; the err_when-chaining idiom suppresses clippy::unused_io_amount — reproduced fresh via a five-variant probe) - rfc1928.md's write_all bullet records both conversions and the real rule (replies are single-shot, all-or-nothing) + the lint blind spot - reject's doc names the 0xff vocabulary choice (RFC 1929 needs only non-0x00); Addr::Domain's deliberating comment pinned Batch 2 (coverage: L-1 + L-2 + L-3 + S-1): - client v6 request encoding pinned (22 bytes, ATYP=4, slice bounds) - Socks5Stream::shutdown() EOF/BrokenPipe propagation pinned - wait_on_tcp's io-error arm + SocksServerError::to_reply_error's full mapping table + read_command's command-phase version mismatch covered - S-1: read_address's 1-byte-field bound documented; decode-side 255/256 boundary pinned both faces Batch 3 (posture notes: C-1, C-2, N-2, N-3 — decided keep-with-note): - ConnectError: hand-port map's companion (cut if the producer layer lands without consuming it); StandardAuthentication: ADR-005 auth-split is the anticipated consumer; CheckResult same shape - tracing: the fork's declared logging seam, noted in Cargo.toml Coverage 92.63% → 93.09% regions / 95.40% → 96.07% lines; the named gaps closed, the remainder is accepted-uncovered with disposition. Review doc carries the re-verification + resolution record. Verification: cargo test (39 lib + 37 differential), clippy (all-targets + wasm32) -D warnings, fmt, doc, wasm check, MSRV 1.88, publish dry-run — all green.
This commit is contained in:
1 parent
4bd25c093e
commit
7bd5d6a7e7
7 files changed
+445
-21
No files matched your search
+70
-2
@@ -141,6 +141,14 @@ impl<T> Socks5ServerProtocol<T, states::Opened> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Closure-result adapter for [`Socks5ServerProtocol
|
||||
/// ::accept_password_auth`]'s credential checker: `bool`,
|
||||
/// `Option<T>`, and `Result<T, E>` all work (`is_good`). In-tree
|
||||
/// callers use `bool` only; the `Option`/`Result` impls anticipate
|
||||
/// the producer layer's username/password mapping (typed rejections
|
||||
/// want `Result`-returning checkers — review 001 N-3). Same
|
||||
/// keep-or-cut decision as `StandardAuthentication` when the producer
|
||||
/// layer lands.
|
||||
pub trait CheckResult {
|
||||
fn is_good(&self) -> bool;
|
||||
}
|
||||
@@ -349,6 +357,14 @@ impl<T: AsyncWrite + Unpin> PasswordAuthenticationImpl<T, password_states::Recei
|
||||
}
|
||||
|
||||
/// Notify the client with a "NOT_ACCEPTABLE" reply and drop the socket.
|
||||
///
|
||||
/// The status byte is `0xff` — RFC 1929 requires only that a
|
||||
/// rejection be any non-`0x00` byte; `0xff` is the convention
|
||||
/// upstream picked (borrowed from the method-negotiation phase's
|
||||
/// `SOCKS5_AUTH_METHOD_NOT_ACCEPTABLE` constant, which is a
|
||||
/// different protocol phase's vocabulary). The bytes interoperate;
|
||||
/// the pairing works only because both in-tree sides write/check
|
||||
/// the same arbitrary constant.
|
||||
pub async fn reject(mut self) -> Result<(), SocksServerError> {
|
||||
self.inner
|
||||
.write_all(&[1, consts::SOCKS5_AUTH_METHOD_NOT_ACCEPTABLE])
|
||||
@@ -434,6 +450,13 @@ auth_method_enums! {
|
||||
/// as an enum appropriate for static dispatch.
|
||||
///
|
||||
/// If you want to add your own custom methods, you can generate a similar enum using the `auth_method_enums` macro.
|
||||
///
|
||||
/// Posture (review 001 C-2): currently uncalled in-tree — the
|
||||
/// typestate's `negotiate_auth` is driven with the single-method
|
||||
/// slices everywhere. Kept as the negotiation machinery's
|
||||
/// combinator: the producer layer's per-door auth selection
|
||||
/// (ADR-005's auth-split) is its anticipated consumer. If the
|
||||
/// producer layer lands without consuming it, cut it then.
|
||||
pub enum StandardAuthentication / StandardAuthenticationStarted<T> {
|
||||
NoAuthentication(NoAuthenticationImpl<T>),
|
||||
PasswordAuthentication(PasswordAuthenticationImpl<T, password_states::Started>),
|
||||
@@ -513,7 +536,7 @@ impl<T: AsyncRead + AsyncWrite + Unpin> Socks5ServerProtocol<T, states::CommandR
|
||||
/// This consumes the wrapper as after this message actual proxying should begin.
|
||||
pub async fn reply_success(mut self, sock_addr: SocketAddr) -> Result<T, SocksServerError> {
|
||||
self.inner
|
||||
.write(&new_reply(&ReplyError::Succeeded, sock_addr))
|
||||
.write_all(&new_reply(&ReplyError::Succeeded, sock_addr))
|
||||
.await
|
||||
.err_when("writing successful reply")?;
|
||||
|
||||
@@ -526,7 +549,7 @@ impl<T: AsyncRead + AsyncWrite + Unpin> Socks5ServerProtocol<T, states::CommandR
|
||||
pub async fn reply_error(mut self, error: &ReplyError) -> Result<(), SocksServerError> {
|
||||
let reply = new_reply(error, V4_UNSPECIFIED_ZERO);
|
||||
self.inner
|
||||
.write(&reply)
|
||||
.write_all(&reply)
|
||||
.await
|
||||
.err_when("writing unsuccessful reply")?;
|
||||
|
||||
@@ -656,6 +679,51 @@ mod tests {
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
/// `SocksServerError::to_reply_error`'s full mapping table (review
|
||||
/// 001 L-3): `UnknownCommand` → `CommandNotSupported`,
|
||||
/// `AddrError` delegates to its own table, and every remaining
|
||||
/// variant falls through to `GeneralFailure` — the mapping the
|
||||
/// producer's error surface relies on when mapping session errors
|
||||
/// to reply codes (`try_notify!`'s secondary-error write). The
|
||||
/// fall-through arms (`Io`/`FromUtf8`/`EOF`/auth variants) are
|
||||
/// exercised here directly, since driving each from the wire is
|
||||
/// not one-test-cheap. Upstream `src/server.rs:62-70`.
|
||||
#[test]
|
||||
fn to_reply_error_mapping_table() {
|
||||
assert!(matches!(
|
||||
SocksServerError::UnknownCommand(0x7f).to_reply_error(),
|
||||
ReplyError::CommandNotSupported
|
||||
));
|
||||
assert!(matches!(
|
||||
SocksServerError::AddrError(crate::rfc1928::AddrError::IncorrectAddressType)
|
||||
.to_reply_error(),
|
||||
ReplyError::AddressTypeNotSupported
|
||||
));
|
||||
for fall_through in [
|
||||
SocksServerError::EOF,
|
||||
SocksServerError::EmptyUsername,
|
||||
SocksServerError::EmptyPassword,
|
||||
SocksServerError::AuthenticationRejected,
|
||||
SocksServerError::UnsupportedSocksVersion(0x04),
|
||||
SocksServerError::Bug("test"),
|
||||
SocksServerError::AuthMethodUnacceptable(vec![0x02]),
|
||||
SocksServerError::UnexpectedUdpControlGarbage(0xde),
|
||||
SocksServerError::Io {
|
||||
source: std::io::Error::other("x"),
|
||||
context: "reading",
|
||||
},
|
||||
SocksServerError::FromUtf8 {
|
||||
source: String::from_utf8(vec![0xff]).unwrap_err(),
|
||||
context: "converting",
|
||||
},
|
||||
] {
|
||||
assert!(matches!(
|
||||
fall_through.to_reply_error(),
|
||||
ReplyError::GeneralFailure
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_reply_v4_form() {
|
||||
let addr: SocketAddr = "127.0.0.1:1080".parse().unwrap();
|
||||
|
||||
@@ -11,6 +11,15 @@
|
||||
//! is a plain `macro_rules!` scoped by `pub(crate) use` so it stays
|
||||
//! internal to the RFC 1928 core (implementation detail, not alksocks
|
||||
//! public API).
|
||||
//!
|
||||
//! [`ConnectError`] is currently constructed nowhere in-tree (review
|
||||
//! 001 C-1 — upstream's sole construction site was the cut
|
||||
//! `tcp_connect` dialer). It is kept as the hand-port map's
|
||||
//! companion: the dial-error → reply-code mapping
|
||||
//! (`to_reply_error`) is what a future kernel-net port (e.g. the
|
||||
//! dial-callback shape, ADR-004) would reuse, and its mapping is
|
||||
//! pinned by the module's test. If the producer layer lands without
|
||||
//! consuming it, cut it then.
|
||||
|
||||
/// Easy to destructure bytes buffers by naming each fields:
|
||||
///
|
||||
|
||||
@@ -204,10 +204,18 @@ impl ToTargetAddr for TargetAddr {
|
||||
pub enum Addr {
|
||||
V4([u8; 4]),
|
||||
V6([u8; 16]),
|
||||
Domain(String), // Vec<[u8]> or Box<[u8]> or String ?
|
||||
// `String` — validated UTF-8 at the codec boundary (`read_address`
|
||||
// converts before constructing).
|
||||
Domain(String),
|
||||
}
|
||||
|
||||
/// This function is used by the client & the server
|
||||
/// This function is used by the client & the server.
|
||||
///
|
||||
/// The domain arm's allocation is bounded by construction: the wire
|
||||
/// length field is one byte, so the claimed length is at most 255 —
|
||||
/// the attacker-controlled byte cannot ask for more. Any change that
|
||||
/// widens that field (an FRAG-style extension) requires an explicit
|
||||
/// cap here before the allocation.
|
||||
pub async fn read_address<T: AsyncRead + Unpin>(
|
||||
stream: &mut T,
|
||||
atyp: u8,
|
||||
|
||||
Reference in new issue
Block a user