Conversation
Ports under 1024 are priveledged and were failing (at least on Linux) when running as a normal user.
We already have oxen-libquic, oxen-logging, and nlohmann via lokinet, so get them via that nested submodule rather than having a duplicated submodule in libsession-util itself. Also removes the macos workaround call to `oxen_logging_add_source_dir` because that directive no longer does anything.
- LOKINET_EMBEDDED=ON is replaced with LOKINET_FULL=OFF - LOKINET_BOOTSTRAP was removed - LOKINET_DAEMON=OFF is not strictly needed (it should be default) but makes it clear what we're doing.
- Load libquic before oxenc/oxen-logging so that libquic has a chance to set up its oxen-logging, etc. targets before libsession tries. - Remove unneeded settings to disable tests/docs (these are [now] the dependency defaults when not doing a top-level project build). - Update to depend on proper lokinet::liblokinet target.
• Added missing config options • Added exponential backoffs for retries (and a retry limit for path building) • Fixed a couple of issues with the logic to finish refreshing the snode pool
• Fixed a use-after-move issue • Fixed an issue where the OnionRequestRouter would start trying to make requests before the SnodePool bootstrap was completed
• Added a missing import • Updated the OnionRequestRouter to wait for the SnodePool to be populated before allowing any requests to be sent • Updated the SnodePool to make ephemeral connections to refresh it's cache (that way we won't always use seed node connections for subsequent requests on new accounts) • Fixed some use-after-move issues • Fixed an issue were the SnodePool bootstrap request response wasn't being handled
• Fixed an infinite loop with the OnionRequestRouter refreshing the SnodePool while a refresh was already running • Fixed an edge-case where the SnodePool wouldn't trigger a refresh when all nodes are marked as failed
• Added parse and expose the general network settings the clients use (network_time_offset, hardfork_version, softfork_version) • Added error handling from old logic • Added 421 retry handling • Fixed an issue where retrying the snode refresh would cause a deadlock
• Added initial LokinetRouter wrapper • Added changes that were missing from previous commit • Updated QuicTransport to be able to send requests to RemoteAddress directly
• Added factory functions for the FileServer endpoints the clients use • Ran the formatter • Fixed a linker error • Fixed a bug where we were incorrectly reporting successful responses as failures
• Added a log when succeeding after a 421 retry (old code had it) • Added logic to mark a node as failed after a QUIC handshake timeout • Added a connection status hook and logic to track the connection status • Added a function to retrieve the current active paths (TODO for the LokinetRouter)
• Added logic so the OnionRequestRouter can observe connection failures to it's guard nodes and trigger path rebuilds when they happen • Fixed an issue where paths in 'single_path_mode' wouldn't get rebuild
| /// Outputs: | ||
| /// - returns true if url contains information indicating deterministic decryption is required; | ||
| /// false otherwise. | ||
| LIBSESSION_EXPORT bool download_url_requires_deterministic_decryption(const char* url); |
There was a problem hiding this comment.
I'm not sure I understand this from the name and the description. For anything we download, the file simply is encrypted and we have no idea (and can never determine) whether it used a deterministic or random seed to perform the encryption, and so for the decryption of a download that information doesn't do anything.
For downloads/decryption, the file decryption is always deterministic because we have to have the key to decrypt it: either we were given the key (and so it is "deterministic" in that it is one exact value) or we somehow derive the key.
(I also think "requires" feels a bit strong since nothing can enforce that, so perhaps "wants" or "uses" would be better, but this is a minor nitpick).
I haven't yet looked at where this is used, but should this be named something like "upload_url_use_deterministic_encryption" instead?
There was a problem hiding this comment.
Ah okay I think I understand now. This is not (exactly) about being deterministic, but really is about whether this is using the new XChaCha20-stream based decryption versus the older all-at-once chunk decryption.
So I think we should rename this C API function to: download_encryption_stream_decrypt or something like that, and similarly rename the C++ member to something like stream_decrypt.
(Partly because there is no reason that this encryption mechanism has to be deterministic: i.e. you can use a full random key to encrypt it and the download will still work. We just make it deterministic because we happen to want that right now, but that could change at some point for some types of downloads while using the same encryption).
There was a problem hiding this comment.
I don't see anything in this PR currently using this; I assume that means its off in the session-ios code, checking this flag and then going into either session::decrypt, and otherwise the old one-shot decryption code?
There was a problem hiding this comment.
I don't see anything in this PR currently using this; I assume that means its off in the session-ios code, checking this flag and then going into either session::decrypt, and otherwise the old one-shot decryption code?
Yep that's right - at the moment the clients include a #d fragment on the download URL to indicate whether it uses the updated encryption or not, iOS calls this function to determine which decryption method to use
Ideally libSession would handle the entire behaviour internally but the platforms currently store attachments inconsistently so I thought it would be better to expose a function that returns the boolean (at least then libSession contains some of the logic)
Hmm... I wonder whether I should be generating the "download url" after the upload and returning that as part of the response (that way libSession would also include the logic to add the fragment values when they differ from the standard)
| ConnectionStatus get_status() const override { return _status.load(); }; | ||
| std::vector<PathInfo> get_active_paths() override; | ||
| std::vector<service_node> get_all_used_nodes() override; | ||
| void send_request(Request request, network_response_callback_t callback) override; |
There was a problem hiding this comment.
This feels like it might be going a little overboard with the shared_ptr usage, and that these would be more appropriately just passed as values (and std::moved into here when called). I don't really see a case where the caller needs to maintain ownership with an active Download/UploadRequest, or whether it could actually do much with such shared ownership.
| std::thread([weak_self = weak_from_this(), | ||
| upload_request = request, | ||
| upload_id, | ||
| file_server_config = _config.file_server_config] { | ||
| auto self = weak_self.lock(); | ||
| if (!self) | ||
| return; |
There was a problem hiding this comment.
Not crucial for this PR by any means, but there's a trick you can use to slightly simplify code that needs to capture a weak ptr like this by capturing both a weak_ptr and this in the lambda. Then everything else in the lambda will already be in object context, so that you don't have to use self-> to get at it. Basically:
auto f = [weak_self = weak_from_this(), this] {
auto self = weak_self.lock();
if (!self)
return;
foo(); // Instead of self->foo();
bar(); // etc.
}
In other words: you capture the this pointer, but you also make sure you get a shared lock on the object before you actually use that pointer, and that's perfectly valid. It makes no difference to how the code actually runs; it's basically just some nice syntatic sugar.
There was a problem hiding this comment.
Does that not strongly capture this?
There was a problem hiding this comment.
this is only a pointer, and so no there's nothing strong here. Whether or not that pointer is still valid to actually use gets determined by whether the weak_ptr locks or not.
There was a problem hiding this comment.
Ah ok good to know - in iOS capturing the equivalent (self) increments a reference counter so it's easy to cause memory leaks doing so, so I figured it was probably the same
There was a problem hiding this comment.
The direct equivalent to that would be capturing a (strong) shared_ptr instead of a weak_ptr in the lambda.
| } | ||
|
|
||
| void OnionRequestRouter::_upload_internal(std::shared_ptr<UploadRequest> request) { | ||
| const auto upload_id = "UP-" + random::random_base32(4); |
There was a problem hiding this comment.
This has the potential to collide. Rare, yes, but if it does the assignment below will end up dropping an in progress upload object. One alternative is just to use a counter:
static std::atomic<int> up_counter = 1, down_counter = 1;
void blablah::_upload_internal(...) {
const auto upload_id = "UP-{}"_format(up_counter++);but if you want to keep the randomness, then something like:
do {
auto [it, ins] = _active_uploads.try_emplace("UP-" + random::random_base32(4), request);
if (ins)
break;
} while (1);will protect again that.
There was a problem hiding this comment.
This bug probably exists in a couple of other places actually 🤦
The original goal of these ids is to make it easy to follow a particular request through debug logs, the randomness was a means to an end for that (a distinct value that won't make the logs too large since it's usually 6-8 chars including the prefix) but at some point I started using it for storing in maps (in a few places...) - might need another approach which works
I'm wondering whether setting up something that combines the two would be beneficial (ie. gets seeded with a random value and then just spits out "incremented" ids)?
That way if someone closes and opens the clients multiple times their logs wouldn't be full of "UP-1" requests and we still remove the risk of collisions because it's always just incrementing a value - eg. "UP-AAAA", "UP-AAAB", etc.
It could also automatically extend the id if it would otherwise wrap around (so "UP-ZZZZ" increments to "UP-AAAAA" or something) for clients that aren't frequently closed (Desktop)
There was a problem hiding this comment.
Using the random tag is a bit nicer for tracking down logs because "X8VM" looks nothing like "JNA7" unlike sequential values "X8VM" and "X8VN". The loop approach there is not really worrying: with 4x32 possible values the chance of it actually looping more than once should be miniscule in any use here.
We could maybe use a random 25 or 30-bit integer instead of a string just for slightly better performance reasons through the maps, and then do string conversion on the fly (being a multiple of 5 bits makes it come out to exactly 6 base32 characters).
The unique map index actually might be actually useful to sort out some of the shared_ptr ownership--i.e. just capture the index instead of the shared_ptr, and then just seeing if we still exist in the parent rather than doing it through the shared ptr. I'm still brainstorming on that idea, though.
| // Accumulate data on a background thread as we don't know whether `next_data` is doing file I/O | ||
| // or just reading from memory (it's a bit of a waste if it's in-memory data but loading from | ||
| // disk should be prioritised) | ||
| std::thread([weak_self = weak_from_this(), |
There was a problem hiding this comment.
Are you certain you want a detached thread here? It feels sort of wrong in a vague way to spawn a thread and throw away the handle to it.
The clean alternative would be to store the thread inside a container in this, and then during destruction call something (e.g. set an atomic bool) to cancel all the downloads, then join() all of them. But there may be a reason that that isn't desirable: the join call will block for as long as the thread does. On the other hand, a detached thread basically gets hard killed (without any object cleanup) when the parent process exits, which is also... a bit unpleasant.
There was a problem hiding this comment.
Happy to change this - the only goal here is to prevent whatever the caller does to provide data from running on the _loop
I think it probably makes sense to store the threads in a contain on this and join() all of them during destruction - if that results in excessive blocking then that would affect the client when trying to destroy the instance which might be desirable anyway as it'd help expose any client-side bugs where next_data blocks excessively
There was a problem hiding this comment.
Yeah needing a thread here is more or less unavoidable.
I looked into alternatives briefly: basically on quite recent Linux kernels you can use io_uring and on modern Windows there is something similar called IOCP. Basically are both kernel interfaces to tell the kernel to go do the IO and notify you when it's done. But anything else basically needs to use threads to do it, and since we aren't going to have massive number of parallel downloads, a thread seems like the easy way to go here everywhere rather than having three different alternatives here.
The file server is a different story, and the streaming version for use with session-router+libquic (see session-foundation/session-file-server#7) does use io_uring to do non-blocking reads from disk without threads. It was a fair bit of work, makes it decidedly linux-only, and doesn't seem remotely worthwhile for the implementation of uploads in Session.
- Since base32 is a power of 2, we can avoid modulus entirely and just
take 5 bits at a time from a random draw.
- Use one RNG draw per 12 chars (60 bits) instead of one per char.
- Make `csrng` inline constexpr instead of extern;
construction/destruction of CSRNG{} is a no-op and an optimizing
compiler ought to notice that and basically optimize it away.
• Tweaked a log to include more error info • Fixed an onion request issue where empty (non-null) bodies would error
src/network/snode_pool.cpp
Outdated
| if (_disk_manager) | ||
| _disk_manager->trigger( | ||
| io_key_write_cache, [path = _snode_cache_file_path, cache = _snode_cache] { | ||
| SnodePool::_perform_cache_write(std::move(path), std::move(cache)); |
There was a problem hiding this comment.
There's a C++ efficiency issue here resulting from the combination of a few things at once:
- captured variables in lambda are const by default inside the lambda body. (There is a
mutablemodifier you can use on the lambda to make them non-const instead). - std::move(x) where x is some
const Tvalue creates aconst T&&reference - which is a sort of weird almost-never-used-directly reference type, but will bind as aconst T&argument when needed. It feels sort of wrong that std::move even allows this, but I suppose it's necessary for generic templated code. - Move constructors (to efficiently move values) take a
T&&argument (not aconst T&&argument), and so if you pass aconst T&&argument to a constructor, that has bothconst T© constructors andT&&move constructors, theconstness means it can only match the copy constructor, and so it invokes a copy. - SnodePool::_perform_cache_write (and similar functions) are taking arguments by value.
Putting all of that together means that these two arguments actually have to get copied (the std::move notwithstanding).
There are a couple ways to fix it:
- Add the
mutablemodifier to the lambda. When you want to be able to change any lambda captures (including being able to properly move them) this is the common way to do it. E.g.[val]() mutable { return val++; }. (Also a subtle note that the constness here is "shallow": if you capture by reference or pointer, only actual the pointer value itself (i.e. the memory location) would be const, but not the thing accessed through the pointer/reference). - If the caller doesn't need to take over the ownership, because it only reads the element during the function call itself, then make it take arguments by
const T& xinstead ofT x.
In these cases, 2) fits the bill, and so I've changed them over to take const references instead of values. (Just leaving this note about why I made that change).
There was a problem hiding this comment.
Ah right, I'm still getting my head around lambda behaviours in C++ - yea the goal here was that the lambda took a copy of the data and then that copy was sent to the function without creating a subsequent copy
I didn't realise that you needed to make the lambda mutable in order to move one of it's captured values but I guess that makes sense since it is modifying the value
Putting all of that together means that these two arguments actually have to get copied (the std::move notwithstanding).
Since the lambda captures a copy do we actually need the move for the function call at all or can we just pass the const reference (since all the function does is need to read the data)?
This better aligns with what session-router does, simplifying things a bit and allows possible future sharing of the disk loop with session router.
- _current_clock_resync_id was never getting reset and so all checks after the first one would always think there was already an ongoing check. - `_clock_resync_results` was being moved, which isn't technically required by the standard to actually clear the vector. Added an explicit clear() call just for safety. - Refactored it slightly to extract the ID and vector in the on_complete function instead of extracting them and them passing them into it.
- Keep strike times as type-safe sys_seconds to avoid needing to cast down from clock to integers in several places - Add utility sysclock_now()/sysclock_now_s()/sysclock_now_ms() to make it less verbose to get a "now" value with a particular precision. - Rework snode pool IO to be a little less raw pointers (using templated functions and string_view consumption rather than direct pointer arithmetic). - Update all raw format integer values to go through oxenc::write_host_as_little so that they are endian-safe (but won't have any overhead compared to memcpy on the vast majority of systems) - Minor cleanups/optimizations
get_service_nodes (since oxen 11) accepts fields as an array of keys, which is a bit less verbose than the old dict of `"key":true` pairs.

This PR refactors the
session_networkto be far more configurable and easier to extend, as well as fix a number of bugs which existed in the original implementation. The main interface has now been genericised with the routing and transport mechanisms abstracted from the client; the updatedRequeststructure also makes it easier to pre-construct requests which should allow for abstracting more of the network requests in the future.It also includes a number of new configuration options, some particularly useful ones include:
Onion Requests,LokinetorDirectto their destinationdevnetenvironmentNote: This contains a breaking change for clients which don't currently use networking -
ENABLE_ONIONREQhas been renamed toENABLE_NETWORKINGto be more accurate (this defaults to on so any clients which have it disabled will need to update their build flag)