Skip to content

Commit 8198c31

Browse files
authored
Merge pull request #114 from dr7ana/manual_routing
No-UDP Endpoints: manual data I/O routing
2 parents 3e89949 + 2f89824 commit 8198c31

File tree

11 files changed

+387
-165
lines changed

11 files changed

+387
-165
lines changed

include/oxen/quic/address.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,8 @@ namespace oxen::quic
380380
return &_path;
381381
}
382382

383+
Path invert() const { return {remote, local}; }
384+
383385
std::string to_string() const;
384386
};
385387
} // namespace oxen::quic

include/oxen/quic/endpoint.hpp

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ namespace oxen::quic
5757
template <typename... Opt>
5858
Endpoint(Network& n, const Address& listen_addr, Opt&&... opts) : net{n}, _local{listen_addr}
5959
{
60-
_init_internals();
6160
((void)handle_ep_opt(std::forward<Opt>(opts)), ...);
61+
_init_internals();
6262
if (_static_secret.empty())
6363
_static_secret = make_static_secret();
6464
}
@@ -88,7 +88,7 @@ namespace oxen::quic
8888
std::promise<std::shared_ptr<Connection>> p;
8989
auto f = p.get_future();
9090

91-
if (!remote.is_addressable())
91+
if (not _manual_routing and !remote.is_addressable())
9292
throw std::invalid_argument("Address must be addressible to connect");
9393

9494
if (_local.is_ipv6() && !remote.is_ipv6())
@@ -195,6 +195,8 @@ namespace oxen::quic
195195
// Returns a random value suitable for use as the Endpoint static secret value.
196196
static ustring make_static_secret();
197197

198+
void manually_receive_packet(Packet&& pkt);
199+
198200
private:
199201
friend class Network;
200202
friend class Loop;
@@ -212,6 +214,8 @@ namespace oxen::quic
212214
Splitting _policy{Splitting::NONE};
213215
int _rbufsize{4096};
214216

217+
opt::manual_routing _manual_routing;
218+
215219
uint64_t _next_rid{0};
216220

217221
ustring _static_secret;
@@ -243,6 +247,7 @@ namespace oxen::quic
243247
void handle_ep_opt(connection_established_callback conn_established_cb);
244248
void handle_ep_opt(connection_closed_callback conn_closed_cb);
245249
void handle_ep_opt(opt::static_secret ssecret);
250+
void handle_ep_opt(opt::manual_routing mrouting);
246251

247252
// Takes a std::optional-wrapped option that does nothing if the optional is empty,
248253
// otherwise passes it through to the above. This is here to allow runtime-dependent
@@ -373,7 +378,7 @@ namespace oxen::quic
373378
void send_or_queue_packet(
374379
const Path& p, std::vector<std::byte> buf, uint8_t ecn, std::function<void(io_result)> callback = nullptr);
375380

376-
void send_version_negotiation(const ngtcp2_version_cid& vid, const Path& p);
381+
void send_version_negotiation(const ngtcp2_version_cid& vid, Path p);
377382

378383
void check_timeouts();
379384

include/oxen/quic/opt.hpp

Lines changed: 157 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -6,138 +6,173 @@
66
#include "crypto.hpp"
77
#include "types.hpp"
88

9-
namespace oxen::quic::opt
9+
namespace oxen::quic
1010
{
11-
using namespace std::chrono_literals;
11+
class Endpoint;
1212

13-
struct max_streams
13+
namespace opt
1414
{
15-
uint64_t stream_count{DEFAULT_MAX_BIDI_STREAMS};
16-
max_streams() = default;
17-
explicit max_streams(uint64_t s) : stream_count{s} {}
18-
};
19-
20-
// If non-zero, this sets a keep-alive timer for outgoing PINGs on this connection so that a
21-
// functioning but idle connection can stay alive indefinitely without hitting the connection's
22-
// idle timeout. Typically in designing a protocol you need only one side to send pings; the
23-
// responses to a ping keep the connection in the other direction alive. This value should
24-
// typically be lower than the idle_timeout of both sides of the connection to be effective.
25-
//
26-
// If this option is not specified or is set to a duration of 0 then outgoing PINGs will not be
27-
// sent on the connection.
28-
struct keep_alive
29-
{
30-
std::chrono::milliseconds time{0ms};
31-
keep_alive() = default;
32-
explicit keep_alive(std::chrono::milliseconds val) : time{val} {}
33-
};
34-
35-
// Can be used to override the default (30s) maximum idle timeout for a connection. Note that
36-
// this is negotiated during connection establishment, and the lower value advertised by each
37-
// side will be used for the connection. Can be 0 to disable idle timeout entirely, but such an
38-
// option has caveats for connections across unknown internet boxes (see comments in RFC 9000,
39-
// section 10.1.2).
40-
struct idle_timeout
41-
{
42-
std::chrono::milliseconds timeout{DEFAULT_IDLE_TIMEOUT};
43-
idle_timeout() = default;
44-
explicit idle_timeout(std::chrono::milliseconds val) : timeout{val} {}
45-
};
46-
47-
/// This can be initialized a few different ways. Simply passing a default constructed struct
48-
/// to Network::Endpoint(...) will enable datagrams without packet-splitting. From there, pass
49-
/// `Splitting::ACTIVE` to the constructor to enable packet-splitting.
50-
///
51-
/// The size of the rotating datagram buffer can also be specified as a second parameter to the
52-
/// constructor. Buffer size is subdivided amongst 4 equally sized buffer rows, so the bufsize
53-
/// must be perfectly divisible by 4
54-
///
55-
/// In some use cases, the user may want the receive data as a string view or a string literal.
56-
/// The default is string literal; setting
57-
///
58-
/// The max size of a transmittable datagram can be queried directly from connection_interface::
59-
/// get_max_datagram_size(). At connection initialization, ngtcp2 will default this value to 1200.
60-
/// The actual value is negotiated upwards via path discovery, reaching a theoretical maximum of
61-
/// NGTCP2_MAX_PMTUD_UDP_PAYLOAD_SIZE (1452), or near it, per datagram. Please note that enabling
62-
/// datagram splitting will double whatever value is returned.
63-
///
64-
/// Note: this setting CANNOT be changed for an endpoint after creation, it must be
65-
/// destroyed and re-initialized with the desired settings.
66-
struct enable_datagrams
67-
{
68-
bool split_packets{false};
69-
Splitting mode{Splitting::NONE};
70-
// Note: this is the size of the entire buffer, divided amongst 4 rows
71-
int bufsize{4096};
72-
73-
enable_datagrams() = default;
74-
explicit enable_datagrams(bool e) = delete;
75-
explicit enable_datagrams(Splitting m) : split_packets{true}, mode{m} {}
76-
explicit enable_datagrams(Splitting m, int b) : split_packets{true}, mode{m}, bufsize{b}
15+
using namespace std::chrono_literals;
16+
17+
struct max_streams
7718
{
78-
if (b <= 0)
79-
throw std::out_of_range{"Bufsize must be positive"};
80-
if (b > 1 << 14)
81-
throw std::out_of_range{"Bufsize too large"};
82-
if (b % 4 != 0)
83-
throw std::invalid_argument{"Bufsize must be evenly divisible between 4 rows"};
84-
}
85-
};
86-
87-
// supported ALPNs for outbound connections
88-
struct outbound_alpns
89-
{
90-
std::vector<ustring> alpns;
91-
explicit outbound_alpns(std::vector<ustring> alpns = {}) : alpns{std::move(alpns)} {}
19+
uint64_t stream_count{DEFAULT_MAX_BIDI_STREAMS};
20+
max_streams() = default;
21+
explicit max_streams(uint64_t s) : stream_count{s} {}
22+
};
9223

93-
// Convenience wrapper that sets a single ALPN value from a regular string:
94-
explicit outbound_alpns(std::string_view alpn) : outbound_alpns{{ustring{to_usv(alpn)}}} {}
95-
};
24+
// supported ALPNs for outbound connections
25+
struct outbound_alpns
26+
{
27+
std::vector<ustring> alpns;
28+
explicit outbound_alpns(std::vector<ustring> alpns = {}) : alpns{std::move(alpns)} {}
9629

97-
// supported ALPNs for inbound connections
98-
struct inbound_alpns
99-
{
100-
std::vector<ustring> alpns;
101-
explicit inbound_alpns(std::vector<ustring> alpns = {}) : alpns{std::move(alpns)} {}
30+
// Convenience wrapper that sets a single ALPN value from a regular string:
31+
explicit outbound_alpns(std::string_view alpn) : outbound_alpns{{ustring{to_usv(alpn)}}} {}
32+
};
10233

103-
// Convenience wrapper that sets a single ALPN value from a regular string:
104-
explicit inbound_alpns(std::string_view alpn) : inbound_alpns{{ustring{to_usv(alpn)}}} {}
105-
};
34+
// supported ALPNs for inbound connections
35+
struct inbound_alpns
36+
{
37+
std::vector<ustring> alpns;
38+
explicit inbound_alpns(std::vector<ustring> alpns = {}) : alpns{std::move(alpns)} {}
10639

107-
// Sets the inbound and outbound ALPNs simulatneous to the same value(s). This is equivalent to
108-
// passing outbound_alpns and inbound_alps, separately, with the same vector argument.
109-
struct alpns
110-
{
111-
std::vector<ustring> inout_alpns;
112-
explicit alpns(std::vector<ustring> alpns = {}) : inout_alpns{std::move(alpns)} {}
40+
// Convenience wrapper that sets a single ALPN value from a regular string:
41+
explicit inbound_alpns(std::string_view alpn) : inbound_alpns{{ustring{to_usv(alpn)}}} {}
42+
};
11343

114-
// Convenience wrapper that sets a single ALPN value from a regular string:
115-
explicit alpns(std::string_view alpn) : alpns{{ustring{to_usv(alpn)}}} {}
116-
};
44+
// Sets the inbound and outbound ALPNs simulatneous to the same value(s). This is equivalent to
45+
// passing outbound_alpns and inbound_alps, separately, with the same vector argument.
46+
struct alpns
47+
{
48+
std::vector<ustring> inout_alpns;
49+
explicit alpns(std::vector<ustring> alpns = {}) : inout_alpns{std::move(alpns)} {}
11750

118-
struct handshake_timeout
119-
{
120-
std::chrono::nanoseconds timeout;
121-
explicit handshake_timeout(std::chrono::nanoseconds ns = 0ns) : timeout{ns} {}
122-
};
123-
124-
// Used to provide precalculated static secret data for an endpoint to use for validation
125-
// tokens. If not provided, 32 random bytes are generated during endpoint construction. The
126-
// data provided must be (at least) SECRET_MIN_SIZE long (longer values are ignored). For a
127-
// deterministic value you should not pass sensitive data here (such as a raw private key), but
128-
// instead use a cryptographically secure hash (ideally with a unique key or suffix) of such
129-
// data.
130-
struct static_secret
131-
{
132-
inline static constexpr size_t SECRET_MIN_SIZE = 16;
51+
// Convenience wrapper that sets a single ALPN value from a regular string:
52+
explicit alpns(std::string_view alpn) : alpns{{ustring{to_usv(alpn)}}} {}
53+
};
13354

134-
ustring secret;
135-
explicit static_secret(ustring s) : secret{std::move(s)}
55+
struct handshake_timeout
56+
{
57+
std::chrono::nanoseconds timeout;
58+
explicit handshake_timeout(std::chrono::nanoseconds ns = 0ns) : timeout{ns} {}
59+
};
60+
61+
// If non-zero, this sets a keep-alive timer for outgoing PINGs on this connection so that a
62+
// functioning but idle connection can stay alive indefinitely without hitting the connection's
63+
// idle timeout. Typically in designing a protocol you need only one side to send pings; the
64+
// responses to a ping keep the connection in the other direction alive. This value should
65+
// typically be lower than the idle_timeout of both sides of the connection to be effective.
66+
//
67+
// If this option is not specified or is set to a duration of 0 then outgoing PINGs will not be
68+
// sent on the connection.
69+
struct keep_alive
70+
{
71+
std::chrono::milliseconds time{0ms};
72+
keep_alive() = default;
73+
explicit keep_alive(std::chrono::milliseconds val) : time{val} {}
74+
};
75+
76+
// Can be used to override the default (30s) maximum idle timeout for a connection. Note that
77+
// this is negotiated during connection establishment, and the lower value advertised by each
78+
// side will be used for the connection. Can be 0 to disable idle timeout entirely, but such an
79+
// option has caveats for connections across unknown internet boxes (see comments in RFC 9000,
80+
// section 10.1.2).
81+
struct idle_timeout
82+
{
83+
std::chrono::milliseconds timeout{DEFAULT_IDLE_TIMEOUT};
84+
idle_timeout() = default;
85+
explicit idle_timeout(std::chrono::milliseconds val) : timeout{val} {}
86+
};
87+
88+
/// This can be initialized a few different ways. Simply passing a default constructed struct
89+
/// to Network::Endpoint(...) will enable datagrams without packet-splitting. From there, pass
90+
/// `Splitting::ACTIVE` to the constructor to enable packet-splitting.
91+
///
92+
/// The size of the rotating datagram buffer can also be specified as a second parameter to the
93+
/// constructor. Buffer size is subdivided amongst 4 equally sized buffer rows, so the bufsize
94+
/// must be perfectly divisible by 4
95+
///
96+
/// In some use cases, the user may want the receive data as a string view or a string literal.
97+
/// The default is string literal; setting
98+
///
99+
/// The max size of a transmittable datagram can be queried directly from connection_interface::
100+
/// get_max_datagram_size(). At connection initialization, ngtcp2 will default this value to 1200.
101+
/// The actual value is negotiated upwards via path discovery, reaching a theoretical maximum of
102+
/// NGTCP2_MAX_PMTUD_UDP_PAYLOAD_SIZE (1452), or near it, per datagram. Please note that enabling
103+
/// datagram splitting will double whatever value is returned.
104+
///
105+
/// Note: this setting CANNOT be changed for an endpoint after creation, it must be
106+
/// destroyed and re-initialized with the desired settings.
107+
struct enable_datagrams
108+
{
109+
bool split_packets{false};
110+
Splitting mode{Splitting::NONE};
111+
// Note: this is the size of the entire buffer, divided amongst 4 rows
112+
int bufsize{4096};
113+
114+
enable_datagrams() = default;
115+
explicit enable_datagrams(bool e) = delete;
116+
explicit enable_datagrams(Splitting m) : split_packets{true}, mode{m} {}
117+
explicit enable_datagrams(Splitting m, int b) : split_packets{true}, mode{m}, bufsize{b}
118+
{
119+
if (b <= 0)
120+
throw std::out_of_range{"Bufsize must be positive"};
121+
if (b > 1 << 14)
122+
throw std::out_of_range{"Bufsize too large"};
123+
if (b % 4 != 0)
124+
throw std::invalid_argument{"Bufsize must be evenly divisible between 4 rows"};
125+
}
126+
};
127+
128+
// Used to provide precalculated static secret data for an endpoint to use for validation
129+
// tokens. If not provided, 32 random bytes are generated during endpoint construction. The
130+
// data provided must be (at least) SECRET_MIN_SIZE long (longer values are ignored). For a
131+
// deterministic value you should not pass sensitive data here (such as a raw private key), but
132+
// instead use a cryptographically secure hash (ideally with a unique key or suffix) of such
133+
// data.
134+
struct static_secret
135+
{
136+
inline static constexpr size_t SECRET_MIN_SIZE = 16;
137+
138+
ustring secret;
139+
explicit static_secret(ustring s) : secret{std::move(s)}
140+
{
141+
if (secret.size() < SECRET_MIN_SIZE)
142+
throw std::invalid_argument{
143+
"opt::static_secret requires data of at least " + std::to_string(SECRET_MIN_SIZE) + "bytes"};
144+
}
145+
};
146+
147+
// Used to provide a callback that bypasses sending packets out through the UDP socket. The passing of
148+
// this opt will also bypass the creation of the UDP socket entirely. The application will also need to
149+
// take responsibility for passing packets into the Endpoint via Endpoint::manually_receive_packet(...)
150+
struct manual_routing
136151
{
137-
if (secret.size() < SECRET_MIN_SIZE)
138-
throw std::invalid_argument{
139-
"opt::static_secret requires data of at least " + std::to_string(SECRET_MIN_SIZE) + "bytes"};
140-
}
141-
};
152+
using send_handler_t = std::function<void(const Path&, bstring_view)>;
153+
154+
private:
155+
friend Endpoint;
156+
157+
manual_routing() = default;
158+
159+
send_handler_t send_hook = nullptr;
160+
161+
public:
162+
explicit manual_routing(send_handler_t cb) : send_hook{std::move(cb)}
163+
{
164+
if (not send_hook)
165+
throw std::runtime_error{"opt::manual_routing must be constructed with a send handler hook!"};
166+
}
167+
168+
io_result operator()(const Path& p, bstring_view data, size_t& n)
169+
{
170+
send_hook(p, data);
171+
n = 0;
172+
return io_result{};
173+
}
142174

143-
} // namespace oxen::quic::opt
175+
explicit operator bool() const { return send_hook != nullptr; }
176+
};
177+
} // namespace opt
178+
} // namespace oxen::quic

include/oxen/quic/udp.hpp

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ extern "C"
1414
#include <event2/event.h>
1515

1616
#include <cstdint>
17+
#include <variant>
1718

1819
#include "address.hpp"
1920
#include "types.hpp"
@@ -33,11 +34,24 @@ namespace oxen::quic
3334
struct Packet
3435
{
3536
Path path;
36-
bstring_view data;
3737
ngtcp2_pkt_info pkt_info{};
38-
39-
/// Constructs a packet from a path and data:
40-
Packet(Path p, bstring_view d) : path{std::move(p)}, data{std::move(d)} {}
38+
std::variant<bstring_view, bstring> pkt_data;
39+
40+
template <typename Char = std::byte, typename = std::enable_if_t<sizeof(Char) == 1>>
41+
std::basic_string_view<Char> data() const
42+
{
43+
return std::visit(
44+
[](const auto& d) {
45+
return std::basic_string_view<Char>{reinterpret_cast<const Char*>(d.data()), d.size()};
46+
},
47+
pkt_data);
48+
}
49+
50+
/// Constructs a packet from a path and data view:
51+
Packet(Path p, bstring_view d) : path{std::move(p)}, pkt_data{std::move(d)} {}
52+
53+
/// Constructs a packet from a path and transferred data:
54+
Packet(Path p, bstring&& d) : path{std::move(p)}, pkt_data{std::move(d)} {}
4155

4256
/// Constructs a packet from a local address, data, and the IP header; remote addr and ECN
4357
/// data are extracted from the header.

0 commit comments

Comments
 (0)