Skip to content

Conversation

@tankyleo
Copy link
Contributor

No description provided.

@ldk-reviews-bot
Copy link

ldk-reviews-bot commented Jan 12, 2026

👋 Thanks for assigning @tnull as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@tankyleo tankyleo force-pushed the 26-01-socks5-outbound branch from a7f498c to 42cbfd4 Compare January 12, 2026 08:36
@tankyleo tankyleo requested a review from tnull January 12, 2026 08:36
SocketAddress::OnionV2 { .. } => unreachable!(),
});

match addr {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Might be a bit cleaner if we do one match addr and then push both the type and the following fields accordingly? If we do that, we could also avoid the unreachable!s below, the early-return if above, and simply return an error for OnionV2?


tcp_stream.write_all(&socks5_request).await.map_err(|_| ())?;

let mut buffer: Vec<u8> = Vec::with_capacity(SOCKS5_REPLY_MIN_LEN);
Copy link
Contributor

@tnull tnull Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will probably immediately re-allocate since the reply should be larger than min reply len in the common case if we read_to_end, no? Should we rather pick a reasonable bufsize for this? Might also make sense to only read the 3 first bytes we look at in the first place?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will probably immediately re-allocate since the reply should be larger than min reply len in the common case if we read_to_end, no?

If we take the common case to be Tor, this reply will always be 10 bytes.

Should we rather pick a reasonable bufsize for this?

I have nonetheless set this to the max possible reply given my reading of the RFC spec.

Might also make sense to only read the 3 first bytes we look at in the first place?

Want to make sure any bytes after the first 3 get discarded. Didn't find a better solution other than just reading them into the buffer and ignoring them.

Copy link
Contributor

@tnull tnull Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have nonetheless set this to the max possible reply given my reading of the RFC spec.

I'm confused, you're saying SOCKS5_REPLY_MIN_LEN is the maximum reply length? Edit: nevermind only now saw that you actually changed the code to include SOCKS5_REQUEST_REPLY_MAX_LEN.

Want to make sure any bytes after the first 3 get discarded. Didn't find a better solution other than just reading them into the buffer and ignoring them.

Hmm, if we're positive this ~always 10 bytes, why not actually discard 10 bytes (or read the fields and keep discarding until we've seen all expected fields)? FWIW, seems we could def. avoid the allocations at all if we make buffer a small fixed-size byte array?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused, you're saying SOCKS5_REPLY_MIN_LEN is the maximum reply length?

I changed the length of the buffer we preallocate to the max length of the reply given my reading of the RFC spec.

Hmm, if we're positive this ~always 10 bytes, why not actually discard 10 bytes (or read the fields and keep discarding until we've seen all expected fields)? FWIW, seems we could def. avoid the allocations at all if we make buffer a small fixed-size byte array?

I want to avoid being too tor-specific here. Another socks5 setup could definitely return more bytes in this response, up to 262 SOCKS5_REQUEST_REPLY_MAX_LEN.

Would you rather we use a [0u8; SOCKS5_REQUEST_REPLY_MAX_LEN] to avoid any heap allocations ? We could make it work yes, but seems to me Vec<u8> is good enough.

tcp_stream.write_all(&socks5_request).await.map_err(|_| ())?;

let mut buffer: Vec<u8> = Vec::with_capacity(SOCKS5_REPLY_MIN_LEN);
let n_read = tcp_stream.read_to_end(&mut buffer).await.map_err(|_| ())?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we read what's available from the stream and then discard the buffer. Are we positive that this would never hold data we actually want to use and bubble up to handlers? As mentioned above, maybe we should just read 3 bytes rather than read_to_end?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we positive that this would never hold data we actually want to use and bubble up to handlers?

In the common case where we talk to Tor, any bytes after this will always be set to ATYP IPV4 and then ADDR 0.0.0.0 and PORT 0. None of this information is useful to callers, so I ignore it. Happy to update this in the future should this information be needed by callers in other SOCKS5 setups.

As mentioned above, maybe we should just read 3 bytes rather than read_to_end?

See above, I want to make sure any bytes after the first 3 get properly discarded, and not read by future calls to read.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the common case where we talk to Tor, any bytes after this will always be set to ATYP IPV4 and then ADDR 0.0.0.0 and PORT 0. None of this information is useful to callers, so I ignore it. Happy to update this in the future should this information be needed by callers in other SOCKS5 setups.

Hmm, if we expect these fields exactly, why not then read the expected number of bytes only, and error out if there's anything unexpected? I'm still wondering if it would be ensured we'd never be able to read actual payload on this first read after connection establishment. FWIW, that kinda violates TCP's streaming model, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not then read the expected number of bytes only, and error out if there's anything unexpected?

As mentioned above, I want to match the socks5 spec, and not constrain the use case to the tor proxy only.

I'm still wondering if it would be ensured we'd never be able to read actual payload on this first read after connection establishment. FWIW, that kinda violates TCP's streaming model, no?

I don't follow this one, can you clarify ?

Copy link
Contributor

@tnull tnull Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't follow this one, can you clarify ?

TCP is a stream-based protocol. If you 'read to end' that only means you read what's currently there to your buffer. That might be the full response, only a part of it, or even the response followed by the some bytes of following packets. See for example Tor's 'optimistic data' mode: given that they allow to pipeline sending requests even when the connection attempt is still inflight could potentially mean that the first response bytes are already queued when we get to read_to_end, so we might end up discarding some valid goodput? It might actually be unlikely, but such races happen, so it would be safer to read only what we need to the buffer?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good point thank you ! See the fixup below, I now read the socks5 reply to the exact byte in case the payload follows immediately in the stream.

@ldk-reviews-bot
Copy link

👋 The first review has been submitted!

Do you think this PR is ready for a second reviewer? If so, click here to assign a second reviewer.

@codecov
Copy link

codecov bot commented Jan 12, 2026

Codecov Report

❌ Patch coverage is 0% with 104 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.51%. Comparing base (c722443) to head (7fdaeb2).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
lightning-net-tokio/src/lib.rs 0.00% 104 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4305      +/-   ##
==========================================
- Coverage   86.59%   86.51%   -0.08%     
==========================================
  Files         158      158              
  Lines      102408   102511     +103     
  Branches   102408   102511     +103     
==========================================
+ Hits        88678    88688      +10     
- Misses      11309    11406      +97     
+ Partials     2421     2417       -4     
Flag Coverage Δ
fuzzing 35.90% <ø> (-0.98%) ⬇️
tests 85.80% <0.00%> (-0.09%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@tankyleo tankyleo force-pushed the 26-01-socks5-outbound branch from 42cbfd4 to e5f6359 Compare January 12, 2026 16:37
@tankyleo tankyleo requested a review from tnull January 12, 2026 16:43
let mut buffer: Vec<u8> = Vec::with_capacity(SOCKS5_REQUEST_REPLY_MAX_LEN);
let n_read = tcp_stream.read_to_end(&mut buffer).await.map_err(|_| ())?;
if n_read < SOCKS5_REQUEST_REPLY_MIN_LEN
|| n_read > SOCKS5_REQUEST_REPLY_MAX_LEN
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this is too strict ? Anything longer than this deviates from the spec, so I lean towards returning Err in that case.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO fine to error if it doesn't adhere to the spec.

Copy link
Contributor Author

@tankyleo tankyleo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also aware we have no tests for this code now, would it be ok to add tests that try connections to actual endpoints on the internet ? ie torproject.org, some famous lightning node onion V3 address...

i have tested socks5_connect in a toy binary and everything works.

@tnull
Copy link
Contributor

tnull commented Jan 12, 2026

Also aware we have no tests for this code now, would it be ok to add tests that try connections to actual endpoints on the internet ? ie torproject.org, some famous lightning node onion V3 address...

i have tested socks5_connect in a toy binary and everything works.

Yeah, tests towards some known endpoints on the internet are notoriously flaky (especially in Github CI), however, they are better than no tests!


tcp_stream.write_all(&socks5_request).await.map_err(|_| ())?;

let mut buffer: Vec<u8> = Vec::with_capacity(SOCKS5_REQUEST_REPLY_MAX_LEN);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned in the other comment: now that we have SOCKS5_REQUEST_REPLY_MAX_LEN, coudl we make this a [u8; SOCKS5_REQUEST_REPLY_MAX_LEN]?

@tankyleo
Copy link
Contributor Author

Yeah, tests towards some known endpoints on the internet are notoriously flaky (especially in Github CI), however, they are better than no tests!

Let me see that would require installing C tor in CI right ? Do we want to do this still ?

@tnull
Copy link
Contributor

tnull commented Jan 12, 2026

Yeah, tests towards some known endpoints on the internet are notoriously flaky (especially in Github CI), however, they are better than no tests!

Let me see that would require installing C tor in CI right ? Do we want to do this still ?

Hmm, it would likely not be too bad, though indeed it might have some overhead if run every time. It would be a good candidate for a nightly CI job, IMO. For now it might be fine to add some tests, if they all reuse the same setup?

// Constants defined in RFC 1928
const VERSION: u8 = 5;
const NMETHODS: u8 = 1;
const NO_AUTH: u8 = 0;
Copy link
Contributor

@tnull tnull Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While NO_AUTH is fine, we should still support setting USERNAME/PASSWORD fields as Tor uses that to let users establish new circuits. FWIW, we might even want to allow users to manually change username/password during runtime for following connections for that reason.

(see https://spec.torproject.org/socks-extensions.html#extent-of-support)

@tankyleo tankyleo force-pushed the 26-01-socks5-outbound branch from e9a864d to 8d22deb Compare January 13, 2026 03:58
- remove heap allocations
- read socks5 reply to the exact byte in case the payload follows
  immediately
- add user-password authentication
- add tests
@tankyleo tankyleo force-pushed the 26-01-socks5-outbound branch from 8d22deb to 7fdaeb2 Compare January 13, 2026 05:20
@tankyleo tankyleo requested a review from tnull January 13, 2026 06:41
Comment on lines +536 to +544
let method_selection_request = [VERSION, NMETHODS, selected_auth];
let mut tcp_stream = TcpStream::connect(&socks5_proxy_addr).await.map_err(|_| ())?;
tcp_stream.write_all(&method_selection_request).await.map_err(|_| ())?;

let mut method_selection_reply = [0u8; METHOD_SELECT_REPLY_LEN];
let n_read = tcp_stream.read_exact(&mut method_selection_reply).await.map_err(|_| ())?;
if n_read != METHOD_SELECT_REPLY_LEN || method_selection_reply != [VERSION, selected_auth] {
return Err(());
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wat? No, just send the CONNECT command and be done with it. We don't need the vast majority of this code.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed offline, HTTP CONNECT is gated behind the HTTPTunnelPort setting, itself not enabled by default in C Tor, so we go with SOCKS5.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants