Skip to content

CVPN-2361: Add batch receive on apple platforms#378

Open
kp-thomas-yau wants to merge 13 commits intomainfrom
add-batch-receive
Open

CVPN-2361: Add batch receive on apple platforms#378
kp-thomas-yau wants to merge 13 commits intomainfrom
add-batch-receive

Conversation

@kp-thomas-yau
Copy link
Copy Markdown
Contributor

@kp-thomas-yau kp-thomas-yau commented Mar 31, 2026

Description

This PR brings speed improvement by switching Apple clients to use recvmsg_x instead of the regular recv_from function. Since the new function allow us to receive a batch of packets all at once, we would need to add something like a buffer to store the packets before outside_io_task polls/fetches a new packet.

I have opted to use rtrb, a SPSC ring buffer that also supports pushing multiple items at once with a fixed capacity. It's wait free and lock-free as well. Once we got packets from recvmsg_x, it will push to the ring buffer all at once via a iterator. On the other hand, when outside_io_task decides to run recv_buf to fetch a packet, it will then pop the ring buffer.

There are cases where the ring buffer will be fully loaded (especially running a speedtest on 10Gbps line). If this happens, the handle_udp_recv tokio task will yield so that it will let tasks like outside_io_task to actually start popping the ring buffer.

Missing in this PR, will do it separately:

  • A function to do a runtime check to verify recvmsg_x does exist
  • Make recv_multiple into a trait so that other platforms like Linux can use the same method as well

Motivation and Context

To bring speed improvements on different Apple platform

How Has This Been Tested?

Tested on a 10Gbps line connecting to one of the testing server with speedtest cli & iPerf3 on a M2 Pro MacBook Pro:

(in Mbps) Speedtest Download Speedtest Upload iPerf3 Download iPerf3 Upload
Current main (db08a68) 943.24 511.78 926 506
This PR 1370.11 519.97 1357 508

On iPhone 17 Pro Max with 5Gbps line:

(in Mbps) Speedtest Download Speedtest Upload Speedtest Download (with Expresslane) Speedtest Upload (with Expresslane)
Current main (db08a68) 1258 595 1316 599
This PR 1517 611 1651 618

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist:

  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • The correct base branch is being used, if not main

Make this a feature to make it opt-in for now. If this feature stabilize later, we can make it part of the code after rounds of rounds of QA tests.
Add a new `enable_batch_receive()` and only call it when `enable_batch_receive` config item is true, so the client-side can enable/disable this feature.
When `batch_receive` is enabled, the outside IO task can no longer rely on polling the socket for readability since packets will be received by a separate recv task using recvmsg_x. Instead, use a Semaphore to signal packet availability — the recv task adds one permit per packet pushed into the recv_queue, and the outside IO task awaits a permit before processing.
@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 31, 2026

Code coverage summary for 063a5eb:

Filename                                                          Regions    Missed Regions     Cover   Functions  Missed Functions  Executed       Lines      Missed Lines     Cover    Branches   Missed Branches     Cover
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
lightway-app-utils/src/args/cipher.rs                                   5                 5     0.00%           1                 1     0.00%           5                 5     0.00%           0                 0         -
lightway-app-utils/src/args/connection_type.rs                          5                 5     0.00%           1                 1     0.00%           5                 5     0.00%           0                 0         -
lightway-app-utils/src/args/duration.rs                                22                18    18.18%           5                 4    20.00%          15                12    20.00%           0                 0         -
lightway-app-utils/src/args/ip_map.rs                                  19                19     0.00%           3                 3     0.00%          13                13     0.00%           0                 0         -
lightway-app-utils/src/args/logging.rs                                 37                37     0.00%           3                 3     0.00%          31                31     0.00%           0                 0         -
lightway-app-utils/src/args/nonzero_duration.rs                        24                16    33.33%           4                 3    25.00%          16                10    37.50%           0                 0         -
lightway-app-utils/src/connection_ticker.rs                           229                17    92.58%          28                 4    85.71%         126                15    88.10%           0                 0         -
lightway-app-utils/src/dplpmtud_timer.rs                              209                13    93.78%          22                 4    81.82%         117                11    90.60%           0                 0         -
lightway-app-utils/src/event_stream.rs                                 19                 0   100.00%           3                 0   100.00%          11                 0   100.00%           0                 0         -
lightway-app-utils/src/packet_codec.rs                                  2                 2     0.00%           1                 1     0.00%           1                 1     0.00%           0                 0         -
lightway-app-utils/src/sockopt/ip_mtu_discover.rs                      81                81     0.00%           6                 6     0.00%          72                72     0.00%           0                 0         -
lightway-app-utils/src/sockopt/ip_pktinfo.rs                           14                14     0.00%           1                 1     0.00%          16                16     0.00%           0                 0         -
lightway-app-utils/src/tun.rs                                         281               281     0.00%          31                31     0.00%         175               175     0.00%           0                 0         -
lightway-app-utils/src/utils.rs                                        21                21     0.00%           1                 1     0.00%          11                11     0.00%           0                 0         -
lightway-client/src/args.rs                                            35                14    60.00%           2                 1    50.00%          17                 8    52.94%           0                 0         -
lightway-client/src/dns_manager.rs                                     18                18     0.00%           4                 4     0.00%          16                16     0.00%           0                 0         -
lightway-client/src/io/inside/tun.rs                                   78                78     0.00%          11                11     0.00%          57                57     0.00%           0                 0         -
lightway-client/src/io/outside.rs                                       2                 2     0.00%           1                 1     0.00%           2                 2     0.00%           0                 0         -
lightway-client/src/io/outside/tcp.rs                                  79                79     0.00%          11                11     0.00%          48                48     0.00%           0                 0         -
lightway-client/src/io/outside/udp.rs                                 121               121     0.00%          13                13     0.00%          76                76     0.00%           0                 0         -
lightway-client/src/keepalive.rs                                      615                44    92.85%          55                 6    89.09%         330                25    92.42%           0                 0         -
lightway-client/src/lib.rs                                            752               617    17.95%          57                47    17.54%         539               450    16.51%           0                 0         -
lightway-client/src/main.rs                                           181               181     0.00%          11                11     0.00%         148               148     0.00%           0                 0         -
lightway-client/src/platform/linux/dns_manager.rs                     101                74    26.73%          14                 9    35.71%          77                52    32.47%           0                 0         -
lightway-client/src/platform/linux/dns_manager/direct_file.rs         106                42    60.38%          12                 6    50.00%          65                29    55.38%           0                 0         -
lightway-client/src/platform/linux/dns_manager/resolvconf.rs           61                61     0.00%           8                 8     0.00%          52                52     0.00%           0                 0         -
lightway-client/src/platform/linux/dns_manager/resolvectl.rs           41                41     0.00%           5                 5     0.00%          34                34     0.00%           0                 0         -
lightway-client/src/route_manager.rs                                 1190               212    82.18%          77                 8    89.61%         663               117    82.35%           0                 0         -
lightway-core/src/borrowed_bytesmut.rs                                373                 0   100.00%          24                 0   100.00%         185                 0   100.00%           0                 0         -
lightway-core/src/builder_predicates.rs                                24                12    50.00%           4                 2    50.00%          24                12    50.00%           0                 0         -
lightway-core/src/cipher.rs                                            13                 0   100.00%           2                 0   100.00%          10                 0   100.00%           0                 0         -
lightway-core/src/connection.rs                                      1605               786    51.03%          71                26    63.38%        1129               523    53.68%           0                 0         -
lightway-core/src/connection/builders.rs                              239                46    80.75%          21                 8    61.90%         247                53    78.54%           0                 0         -
lightway-core/src/connection/dplpmtud.rs                             1741                81    95.35%          63                 0   100.00%         830                 7    99.16%           0                 0         -
lightway-core/src/connection/fragment_map.rs                          366                 6    98.36%          25                 0   100.00%         254                 3    98.82%           0                 0         -
lightway-core/src/connection/io_adapter.rs                            526                23    95.63%          34                 5    85.29%         276                21    92.39%           0                 0         -
lightway-core/src/connection/key_update.rs                             34                13    61.76%           5                 0   100.00%          38                19    50.00%           0                 0         -
lightway-core/src/context.rs                                          202                45    77.72%          26                 9    65.38%         213                52    75.59%           0                 0         -
lightway-core/src/context/ip_pool.rs                                    8                 3    62.50%           1                 0   100.00%           5                 0   100.00%           0                 0         -
lightway-core/src/context/server_auth.rs                               32                24    25.00%           4                 3    25.00%          24                20    16.67%           0                 0         -
lightway-core/src/encoding_request_states.rs                            3                 0   100.00%           1                 0   100.00%           3                 0   100.00%           0                 0         -
lightway-core/src/io.rs                                                10                10     0.00%           3                 3     0.00%           9                 9     0.00%           0                 0         -
lightway-core/src/lib.rs                                                9                 0   100.00%           3                 0   100.00%           9                 0   100.00%           0                 0         -
lightway-core/src/metrics.rs                                           51                41    19.61%          21                17    19.05%          48                38    20.83%           0                 0         -
lightway-core/src/packet.rs                                            38                10    73.68%           4                 1    75.00%          30                 6    80.00%           0                 0         -
lightway-core/src/plugin.rs                                           303                13    95.71%          21                 3    85.71%         145                 7    95.17%           0                 0         -
lightway-core/src/utils.rs                                            334                26    92.22%          22                 2    90.91%         165                17    89.70%           0                 0         -
lightway-core/src/version.rs                                           93                 0   100.00%          17                 0   100.00%          82                 0   100.00%           0                 0         -
lightway-core/src/wire.rs                                             435                39    91.03%          28                 0   100.00%         232                 9    96.12%           0                 0         -
lightway-core/src/wire/auth_failure.rs                                 27                 1    96.30%           3                 0   100.00%          17                 0   100.00%           0                 0         -
lightway-core/src/wire/auth_request.rs                                472                12    97.46%          26                 0   100.00%         241                 0   100.00%           0                 0         -
lightway-core/src/wire/auth_success_with_config_ipv4.rs               222                 3    98.65%          11                 0   100.00%         124                 0   100.00%           0                 0         -
lightway-core/src/wire/data.rs                                         51                 1    98.04%           5                 0   100.00%          33                 0   100.00%           0                 0         -
lightway-core/src/wire/data_frag.rs                                   127                 1    99.21%          14                 0   100.00%          80                 0   100.00%           0                 0         -
lightway-core/src/wire/encoding_request.rs                             82                 2    97.56%           6                 0   100.00%          42                 1    97.62%           0                 0         -
lightway-core/src/wire/encoding_response.rs                            82                 2    97.56%           6                 0   100.00%          42                 1    97.62%           0                 0         -
lightway-core/src/wire/expresslane_config.rs                          166                 1    99.40%           8                 0   100.00%          92                 0   100.00%           0                 0         -
lightway-core/src/wire/expresslane_data.rs                            995                50    94.97%          39                 4    89.74%         498                27    94.58%           0                 0         -
lightway-core/src/wire/ping.rs                                         95                 2    97.89%           7                 0   100.00%          59                 0   100.00%           0                 0         -
lightway-core/src/wire/pong.rs                                        109                 2    98.17%           8                 0   100.00%          72                 0   100.00%           0                 0         -
lightway-core/src/wire/server_config.rs                                69                 2    97.10%           4                 0   100.00%          37                 0   100.00%           0                 0         -
lightway-server/src/auth.rs                                           273                49    82.05%          22                 6    72.73%         171                28    83.63%           0                 0         -
lightway-server/src/connection.rs                                     137               137     0.00%          11                11     0.00%         110               110     0.00%           0                 0         -
lightway-server/src/connection_manager.rs                             330               330     0.00%          39                39     0.00%         278               278     0.00%           0                 0         -
lightway-server/src/connection_manager/connection_map.rs              407                20    95.09%          21                 1    95.24%         228                 8    96.49%           0                 0         -
lightway-server/src/io/inside/tun.rs                                   44                44     0.00%           9                 9     0.00%          30                30     0.00%           0                 0         -
lightway-server/src/io/outside/tcp.rs                                  90                90     0.00%           9                 9     0.00%          65                65     0.00%           0                 0         -
lightway-server/src/io/outside/udp.rs                                 320               320     0.00%          16                16     0.00%         206               206     0.00%           0                 0         -
lightway-server/src/io/outside/udp/cmsg.rs                            197                53    73.10%          14                 5    64.29%         158                43    72.78%           0                 0         -
lightway-server/src/ip_manager.rs                                     542                50    90.77%          22                 4    81.82%         242                23    90.50%           0                 0         -
lightway-server/src/ip_manager/ip_pool.rs                             540                 0   100.00%          27                 0   100.00%         252                 0   100.00%           0                 0         -
lightway-server/src/lib.rs                                            251               251     0.00%          14                14     0.00%         169               169     0.00%           0                 0         -
lightway-server/src/main.rs                                           218               218     0.00%          10                10     0.00%         120               120     0.00%           0                 0         -
lightway-server/src/metrics.rs                                        254               250     1.57%          85                83     2.35%         215               211     1.86%           0                 0         -
lightway-server/src/statistics.rs                                     132                59    55.30%           8                 4    50.00%          86                35    59.30%           0                 0         -
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
TOTAL                                                               17019              5241    69.21%        1235               498    59.68%       10363              3642    64.86%           0                 0         -

✅ Region coverage 69% passes
✅ Line coverage 64% passes

Wrapping Arc in this case prevents modification after we create the Socket object, so we now wrap the socket with Arc before we return `outside_io` in the main connection function.
Stop clogging Udp struct by adding fields and logics by moving them to a new file instead for cleaner look.
…ng buffer

`handle_udp_recv` runs when the socket is readable and attempts to receive multiple packets all at once. If it succeeds, we push all the packets at once via `write_chunk_uninit` by filling the ring buffer with an iterator to save time. Also add a `BatchReceiverConsumerError` error to return to so that we can propagate the error properly when it early exits.
Fixed a bug where we're reading MAX_OUTSIDE_MTU for every outside packet and read from the actual message data length.
Discovered a bug while stress-testing (with speed test) on a 10Gbps line where the ring buffer quickly gets overwhelmed and became full. If it's full, the tokio select function will constantly loop (as there's packets that is readable in kernel already) and not yielding control back to the scheduler. It will starve outside_io_task and causing none of the packets to be processed.

So we force it to yield now so that tokio task scheduler will pick up other tasks.
@kp-thomas-yau kp-thomas-yau force-pushed the add-batch-receive branch 2 times, most recently from 318c420 to 61f4d8d Compare March 31, 2026 12:22
@kp-thomas-yau kp-thomas-yau changed the title CVPN-2361: Add batch receive on macOS CVPN-2361: Add batch receive on apple platforms Mar 31, 2026
Add unit tests for both `BatchReceiver` struct and `handle_udp_recv`. Also update CI to run the unit tests as well
Use self.sock.as_ref() so pmtud helpers receive &UdpSocket instead of &Arc<UdpSocket>, which does not implement AsRawSocket on Windows.
@kp-thomas-yau kp-thomas-yau marked this pull request as ready for review April 1, 2026 06:21
@kp-thomas-yau kp-thomas-yau requested a review from a team as a code owner April 1, 2026 06:21
Copy link
Copy Markdown
Contributor

@kp-mariappan-ramasamy kp-mariappan-ramasamy left a comment

Choose a reason for hiding this comment

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

It is well done PR, Thanks

Changes in general LGTM

I was also thinking whether it would be nicer to push this "recv_multiple" concept further up in the chain to OutsideIO like:

 /// Receive multiple packets at once. Each received packet is placed into
 /// the corresponding slot of `bufs`. Returns the number of packets
 /// received. The default receives a single packet via [`recv_buf`](Self::recv_buf).
 fn recv_multiple_buf(&self, bufs: &mut [bytes::BytesMut]) -> IOCallbackResult<usize>;

So application can use it if this support is enabled

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

we can get rid of this commit e9b6b74419606e5667945e2e4eef958b997db740
clippy: Only import 'IpPmtudisc' on Windows

Comment on lines +22 to +24
/// `poll(tokio::io::Interest::READABLE)` on the socket itself. But, if this client
/// is built with `batch_receive` and `enable_batch_receive` is set to true in the config,
/// it will instead try to acquire a permit of a Semaphore.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We could move the comment about batch_receive to the actual function instead of trait definition, since it is just batch_receive's implementaion detail and can change later.

But, if this client
    /// is built with `batch_receive` and `enable_batch_receive` is set to true in the config,
    /// it will instead try to acquire a permit of a Semaphore.

Comment on lines +182 to +188
// Not supported yet
// fn sendmsg_x(
// s: libc::c_int,
// msgp: *const msghdr_x,
// cnt: libc::c_uint,
// flags: libc::c_int,
// ) -> isize;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We can delete this and add later when required.

Comment on lines +128 to +129
Err(e) if e.kind() == io::ErrorKind::WouldBlock => break 0,
Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What is the difference between break 0 and continue ?
It looks like both behave similar since we do not have anything after the loop.

Just curious, if you wanted to handle anything else by this differentiation

io_error: Arc<Mutex<Option<io::Error>>>,
) {
let mut recv_bufs = [[0u8; MAX_OUTSIDE_MTU]; BATCH_SIZE];
let mut msg_lens = [0usize; BATCH_SIZE];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
let mut msg_lens = [0usize; BATCH_SIZE];
let mut recv_bufs: [BytesMut; BATCH_SIZE] =
std::array::from_fn(|_| BytesMut::with_capacity(MAX_OUTSIDE_MTU));

We could consider using BytesMut as array instead of static buffer to avoid the copying at line
https://github.com/expressvpn/lightway/pull/378/changes#diff-31acaecb4daa8d8ab8fbe35347045c8df09d58bf3520e98681d757ce94967a34R144
Later when we need to take the buffer int the above location, we can replace it like

std::mem::replace(&mut recv_bufs[i],BytesMut::with_capacity(MAX_OUTSIDE_MTU),)

Prior art in WolfSsl
https://github.com/expressvpn/wolfssl-rs/blob/427c6f668966f47a9fc3f66287f9231dd3a07faf/wolfssl/src/ssl.rs#L517-L524

return match receiver.pop_recv_consumer() {
Ok(b) => {
let len = b.len();
buf.put_slice(&b[..]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We could just take the recv buffer directly and drop the incoming buffer
It will avoid the copying of entire buffer

Suggested change
buf.put_slice(&b[..]);
*buf = b;

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.

2 participants