Coverage Report

Created: 2026-07-30 07:14

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/libtorrent/fuzzers/src/socks5_udp.cpp
Line
Count
Source
1
/*
2
3
Copyright (c) 2026, Arvid Norberg
4
All rights reserved.
5
6
You may use, distribute and modify this code under the terms of the BSD license,
7
see LICENSE file.
8
*/
9
10
// Fuzzer for the SOCKS5 UDP path (UDP ASSOCIATE). It exercises:
11
//
12
//   - The control plane: the socks5 state machine that lives inside
13
//     udp_socket.cpp, driving every supported tunnel configuration
14
//     (no-auth vs. user/password, send_local_ep on/off, IPv4 vs.
15
//     DOMAINNAME bind address in the UDP ASSOCIATE response, and the
16
//     proxy_peer/proxy_tracker_connections flags).
17
//
18
//   - The data plane: udp_socket::wrap() and udp_socket::unwrap()
19
//     receive arbitrary fuzzer bytes once the tunnel is active. Wrap
20
//     is exercised both by endpoint and by hostname (and across both
21
//     IPv4 and IPv6 destinations); unwrap is exercised by injecting a
22
//     UDP packet from a fake SOCKS5 relay socket bound to the address
23
//     advertised in the tunnel handshake.
24
//
25
// Per-input wire format:
26
//
27
//   Byte 0 : configuration flags (see flag_* constants below)
28
//   Bytes 1.. : raw bytes used as the body of:
29
//                 - the UDP packet injected from the fake relay
30
//                   (drives unwrap() through udp_socket::read())
31
//                 - the optional send()/send_hostname() calls
32
//                   (drives wrap() in both endpoint and hostname forms)
33
//
34
// The TCP transcript that the fake SOCKS5 proxy server returns is canned
35
// (built deterministically from the flags) so that the tunnel reaches the
36
// active state and both wrap and unwrap actually run.
37
38
#include <array>
39
#include <cassert>
40
#include <chrono>
41
#include <cstdint>
42
#include <cstdio>
43
#include <cstring>
44
#include <memory>
45
#include <optional>
46
#include <thread>
47
#include <vector>
48
#include <string>
49
50
#include "libtorrent/io_context.hpp"
51
#include "libtorrent/socket.hpp"
52
#include "libtorrent/error_code.hpp"
53
#include "libtorrent/settings_pack.hpp"
54
#include "libtorrent/span.hpp"
55
#include "libtorrent/address.hpp"
56
57
#include "libtorrent/aux_/udp_socket.hpp"
58
#include "libtorrent/aux_/listen_socket_handle.hpp"
59
#include "libtorrent/aux_/alert_manager.hpp"
60
#include "libtorrent/aux_/resolver_interface.hpp"
61
#include "libtorrent/aux_/proxy_settings.hpp"
62
#include "libtorrent/aux_/session_impl.hpp" // for aux::listen_socket_t
63
64
#include "libtorrent/aux_/disable_warnings_push.hpp"
65
#include <boost/asio/write.hpp>
66
#include "libtorrent/aux_/disable_warnings_pop.hpp"
67
68
namespace {
69
70
  // Configuration flag bits in the first input byte.
71
  constexpr std::uint8_t flag_socks5_pw = 0x01; // auth = socks5_pw (else socks5)
72
  constexpr std::uint8_t flag_with_credentials = 0x02; // populate username/password
73
  constexpr std::uint8_t flag_send_local_ep = 0x04; // udp_socket send_local_ep argument
74
  constexpr std::uint8_t flag_proxy_peer = 0x08; // proxy_peer_connections
75
  constexpr std::uint8_t flag_proxy_tracker = 0x10; // proxy_tracker_connections
76
  constexpr std::uint8_t flag_atyp_domain =
77
    0x20; // UDP ASSOCIATE response uses DOMAINNAME atyp (else IPv4)
78
  constexpr std::uint8_t flag_server_offers_pw =
79
    0x40; // server's method-selection METHOD = 0x02 (vs 0x00)
80
  constexpr std::uint8_t flag_server_auth_fail =
81
    0x80; // sub-negotiation STATUS = 0x01 fail (vs 0x00 success)
82
83
  // A minimal synchronous resolver that returns an inline result. asio's real
84
  // resolver dispatches lookups through an internal thread, which adds latency
85
  // and shutdown coordination that the fuzzer does not need; "127.0.0.1" is the
86
  // only hostname we ever ask about, and make_address() handles that without
87
  // touching the network.
88
  struct fuzz_resolver final : lt::aux::resolver_interface
89
  {
90
    void async_resolve(std::string const& host, lt::aux::resolver_flags, callback_t h) override
91
72
    {
92
72
      lt::error_code ec;
93
72
      std::vector<lt::address> ips;
94
72
      lt::address const a = lt::make_address(host, ec);
95
72
      if (!ec) ips.push_back(a);
96
72
      h(ec, ips);
97
72
    }
98
0
    void abort() override {}
99
0
    void set_cache_timeout(lt::seconds) override {}
100
  };
101
102
  // Pump the io_context until idle. Bounded so a misbehaving handler chain
103
  // can never wedge the fuzzer. Returns true if the iteration budget was
104
  // exhausted (i.e. the queue was still non-empty), false if the queue
105
  // drained naturally. For the workloads this fuzzer drives, the bounded
106
  // exit should never trigger -- so the caller treats it as a hard error.
107
  bool drain(lt::io_context& ios, int const max_iters = 200)
108
144
  {
109
264
    for (int i = 0; i < max_iters; ++i)
110
264
    {
111
264
      ios.restart();
112
264
      if (ios.poll() == 0) return false;
113
264
    }
114
0
    return true;
115
144
  }
116
117
  void drain_or_die(lt::io_context& ios, int const max_iters = 200)
118
144
  {
119
144
    if (drain(ios, max_iters))
120
0
    {
121
0
      std::fprintf(stderr,
122
0
        "socks5_udp fuzzer: drain() exhausted %d iterations without"
123
0
        " emptying the io_context -- handler chain bug?\n",
124
0
        max_iters);
125
0
      assert(false && "drain() exhausted iteration budget");
126
0
    }
127
144
  }
128
129
  // Build the canned TCP transcript that the fake SOCKS5 proxy will write back
130
  // in response to the udp_socket's outgoing handshake. The server's choices
131
  // (which auth method to offer, whether sub-negotiation succeeds) are
132
  // independent of the client's configuration so that the fuzzer can explore
133
  // the disagreement matrix -- e.g. client expects pw auth but server picks
134
  // no auth, or sub-negotiation returns failure. When server's choices line
135
  // up with the client's, the tunnel reaches the active state with
136
  // m_udp_proxy_addr = (127.0.0.1, relay_port) and subsequent relay packets
137
  // get past the source-IP match in udp_socket::read().
138
  std::vector<std::uint8_t> build_proxy_transcript(bool const server_offers_pw,
139
    bool const server_auth_fail,
140
    bool const atyp_domain,
141
    std::uint16_t const relay_port)
142
72
  {
143
72
    std::vector<std::uint8_t> t;
144
145
    // Method-selection response.
146
72
    t.push_back(0x05); // VER = SOCKS5
147
72
    t.push_back(server_offers_pw ? 0x02 : 0x00); // METHOD = user/pw or no auth
148
149
    // Username/password sub-negotiation response (only emitted if the
150
    // server told the client to authenticate). The client only reads
151
    // these bytes when it actually entered the sub-negotiation phase;
152
    // otherwise the bytes pile up in the connection and are discarded
153
    // when the client closes the socket.
154
72
    if (server_offers_pw)
155
36
    {
156
36
      t.push_back(0x01); // VER = 1
157
36
      t.push_back(server_auth_fail ? 0x01 : 0x00); // STATUS
158
36
    }
159
160
    // UDP ASSOCIATE response. atyp=1 (IPv4) or atyp=3 (DOMAINNAME).
161
72
    t.push_back(0x05); // VER
162
72
    t.push_back(0x00); // REP = succeeded
163
72
    t.push_back(0x00); // RSV
164
72
    if (atyp_domain)
165
39
    {
166
39
      t.push_back(0x03); // ATYP = DOMAINNAME
167
39
      std::string const name = "127.0.0.1";
168
39
      t.push_back(static_cast<std::uint8_t>(name.size()));
169
39
      t.insert(t.end(), name.begin(), name.end());
170
39
    }
171
33
    else
172
33
    {
173
33
      t.push_back(0x01); // ATYP = IPv4
174
33
      t.push_back(0x7f);
175
33
      t.push_back(0x00);
176
33
      t.push_back(0x00);
177
33
      t.push_back(0x01); // 127.0.0.1
178
33
    }
179
72
    t.push_back(static_cast<std::uint8_t>(relay_port >> 8));
180
72
    t.push_back(static_cast<std::uint8_t>(relay_port & 0xff));
181
182
72
    return t;
183
72
  }
184
185
} // anonymous namespace
186
187
extern "C" int LLVMFuzzerTestOneInput(std::uint8_t const* data, std::size_t size)
188
72
{
189
72
  if (size < 1) return 0;
190
191
72
  std::uint8_t const flags = data[0];
192
72
  std::uint8_t const* body = data + 1;
193
72
  std::size_t const body_len = size - 1;
194
195
72
  bool const socks5_pw = (flags & flag_socks5_pw) != 0;
196
72
  bool const with_credentials = (flags & flag_with_credentials) != 0;
197
72
  bool const send_local_ep = (flags & flag_send_local_ep) != 0;
198
72
  bool const proxy_peer = (flags & flag_proxy_peer) != 0;
199
72
  bool const proxy_tracker = (flags & flag_proxy_tracker) != 0;
200
72
  bool const atyp_domain = (flags & flag_atyp_domain) != 0;
201
72
  bool const server_offers_pw = (flags & flag_server_offers_pw) != 0;
202
72
  bool const server_auth_fail = (flags & flag_server_auth_fail) != 0;
203
204
72
  lt::io_context ios;
205
72
  lt::aux::alert_manager alerts(100);
206
72
  fuzz_resolver resolver;
207
208
  // listen_socket_t supplies the local endpoint and can_route() that the
209
  // socks5 state machine queries. We only need a v4 loopback address; the
210
  // netmask is wide enough that can_route() accepts the proxy's address.
211
72
  auto listen_sock = std::make_shared<lt::aux::listen_socket_t>();
212
72
  listen_sock->local_endpoint = lt::tcp::endpoint(lt::make_address_v4("127.0.0.1"), 0);
213
72
  listen_sock->netmask = lt::make_address_v4("255.0.0.0");
214
72
  lt::aux::listen_socket_handle ls(listen_sock);
215
216
72
  lt::error_code ec;
217
218
  // Fake SOCKS5 proxy server (TCP).
219
72
  lt::tcp::acceptor proxy_tcp(ios);
220
72
  proxy_tcp.open(lt::tcp::v4(), ec);
221
72
  if (ec) return 0;
222
72
  proxy_tcp.bind(lt::tcp::endpoint(lt::make_address_v4("127.0.0.1"), 0), ec);
223
72
  if (ec) return 0;
224
72
  proxy_tcp.listen(boost::asio::socket_base::max_listen_connections, ec);
225
72
  if (ec) return 0;
226
72
  std::uint16_t const proxy_port = proxy_tcp.local_endpoint().port();
227
228
  // Fake SOCKS5 UDP relay -- "the proxy's UDP-side socket". UDP packets
229
  // from this endpoint look authentic to the udp_socket because the socks5
230
  // handshake response (built below) advertises this exact port.
231
72
  lt::udp::socket relay(ios);
232
72
  relay.open(lt::udp::v4(), ec);
233
72
  if (ec) return 0;
234
72
  relay.bind(lt::udp::endpoint(lt::make_address_v4("127.0.0.1"), 0), ec);
235
72
  if (ec) return 0;
236
72
  relay.non_blocking(true, ec);
237
72
  if (ec) return 0;
238
  // Sanity check: the source-IP comparison in udp_socket::read()
239
  // depends on relay's outgoing packets carrying 127.0.0.1 as the
240
  // source. That is only guaranteed when relay is actually bound to
241
  // 127.0.0.1; assert it so a future regression here fails loud.
242
72
  assert(relay.local_endpoint().address() == lt::make_address_v4("127.0.0.1"));
243
72
  std::uint16_t const relay_port = relay.local_endpoint().port();
244
245
  // The udp_socket under test.
246
72
  lt::aux::udp_socket us(ios, ls);
247
72
  us.open(lt::udp::v4(), ec);
248
72
  if (ec) return 0;
249
72
  us.bind(lt::udp::endpoint(lt::make_address_v4("127.0.0.1"), 0), ec);
250
72
  if (ec) return 0;
251
252
  // Set up the proxy-side acceptor: when the udp_socket's socks5 state
253
  // machine connects, write our canned transcript.
254
  //
255
  // transcript and proxy_side are owned by shared_ptrs that the handler
256
  // chain captures by value, so their storage outlives any pending async
257
  // operation regardless of how cleanup is reordered. async_write keeps
258
  // a (ptr, size) buffer descriptor into transcript's storage until the
259
  // op completes or is cancelled, so transcript must remain valid that
260
  // long -- the shared_ptr in the completion handler guarantees it.
261
72
  auto transcript = std::make_shared<std::vector<std::uint8_t> const>(
262
72
    build_proxy_transcript(server_offers_pw, server_auth_fail, atyp_domain, relay_port));
263
72
  auto proxy_side = std::make_shared<std::optional<lt::tcp::socket>>();
264
72
  proxy_tcp.async_accept([transcript, proxy_side](lt::error_code const& aec, lt::tcp::socket s) {
265
72
    if (aec) return;
266
72
    proxy_side->emplace(std::move(s));
267
72
    boost::asio::async_write(**proxy_side,
268
72
      boost::asio::buffer(*transcript),
269
72
      [transcript](lt::error_code const&, std::size_t) {});
270
72
  });
271
272
  // Configure the proxy and kick off the handshake.
273
72
  lt::aux::proxy_settings ps;
274
72
  ps.hostname = "127.0.0.1";
275
72
  ps.port = proxy_port;
276
72
  ps.type = socks5_pw ? lt::settings_pack::socks5_pw : lt::settings_pack::socks5;
277
72
  if (with_credentials)
278
33
  {
279
33
    ps.username = "fuzz-user";
280
33
    ps.password = "fuzz-pass";
281
33
  }
282
72
  ps.proxy_peer_connections = proxy_peer;
283
72
  ps.proxy_tracker_connections = proxy_tracker;
284
285
72
  us.set_proxy_settings(ps, alerts, resolver, send_local_ep);
286
287
  // Drive the handshake to completion (or to an error). The fuzz_resolver
288
  // returns inline so the only async work is the loopback TCP traffic.
289
72
  drain_or_die(ios);
290
291
  // Data plane: drive unwrap() with arbitrary bytes from the relay.
292
72
  bool packet_in_flight = false;
293
72
  if (us.active_socks5() && body_len > 0)
294
32
  {
295
32
    relay.send_to(boost::asio::buffer(body, body_len), us.local_endpoint(), 0, ec);
296
32
    packet_in_flight = !ec;
297
32
  }
298
299
  // Pull the packet through udp_socket::read(). This is the production
300
  // code path that calls unwrap() on every packet whose source matches
301
  // the proxy's advertised relay endpoint. read() does a synchronous
302
  // receive_from() on a non-blocking socket, so it may return
303
  // would_block if the kernel hasn't placed the packet on the receive
304
  // queue yet. Retry briefly to absorb that delivery jitter, but only
305
  // when we know a packet is actually in flight -- otherwise the vast
306
  // majority of inputs (those that never reach active_socks5()) would
307
  // pay the full sleep budget for nothing.
308
  // Note: udp_socket::read() returning 0 here is the common case, not
309
  // an error. socks5_unwrap() rejects any packet that isn't shaped
310
  // like a valid SOCKS5 UDP header, and most random fuzzer payloads
311
  // aren't -- that rejection is exactly the unwrap() coverage we want.
312
  // We can't distinguish "kernel drop" / "source IP mismatch" /
313
  // "unwrap rejected" from outside read(), so we don't try.
314
72
  if (packet_in_flight)
315
24
  {
316
24
    std::array<lt::aux::udp_socket::packet, 4> pkts;
317
312
    for (int i = 0; i < 16; ++i)
318
294
    {
319
294
      lt::error_code rec;
320
294
      int const n = us.read(pkts, rec);
321
294
      if (n > 0) break;
322
288
      if (rec != boost::asio::error::would_block && rec != boost::asio::error::try_again)
323
0
        break;
324
288
      using namespace std::chrono_literals;
325
288
      std::this_thread::sleep_for(100us);
326
288
    }
327
24
  }
328
329
  // Data plane: drive wrap() through send() and send_hostname() with the
330
  // fuzz buffer as the payload. All three forms run on every input so
331
  // every wrap variant gets coverage from every byte mutation. When
332
  // active_socks5() is true these end up at our relay; the kernel queues
333
  // the writes so the buffers stay alive past the call site.
334
72
  {
335
72
    lt::span<char const> const payload(
336
72
      reinterpret_cast<char const*>(body), static_cast<std::ptrdiff_t>(body_len));
337
338
    // IPv4 destination through wrap-by-endpoint.
339
72
    {
340
72
      lt::udp::endpoint dest(lt::make_address_v4("8.8.8.8"), 1234);
341
72
      us.send(dest, payload, ec);
342
72
    }
343
    // IPv6 destination through wrap-by-endpoint: exercises the v6
344
    // branch of write_endpoint() and the dont_fragment v4-only check.
345
72
    {
346
72
      lt::udp::endpoint dest6(lt::make_address_v6("::1"), 1234);
347
72
      us.send(dest6, payload, ec);
348
72
    }
349
    // Hostname destination through wrap-by-hostname.
350
72
    us.send_hostname("example.com", 1234, payload, ec);
351
72
  }
352
353
  // Cleanup. close() on the udp_socket also closes its inner socks5
354
  // connection; any read/write handlers still pending get cancelled and
355
  // drain() runs them so they don't fire across invocations.
356
72
  us.close();
357
72
  if (proxy_side->has_value()) (**proxy_side).close(ec);
358
72
  proxy_tcp.close(ec);
359
72
  relay.close(ec);
360
72
  drain_or_die(ios, 500);
361
362
72
  return 0;
363
72
}