Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add callback for unhandled STUN requests #1211

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

achingbrain
Copy link
Contributor

@achingbrain achingbrain commented Jun 14, 2024

Calls the functions added to libjuice in paullouisageneau/libjuice#248 (this needs to be merged & released before CI will pass here).

Exports a OnUnhandledStunRequest function that can be passed a callback that will be invoked when an incoming STUN message is received that has no corresponding agent for the ICE ufrag.

Refs #1166

@achingbrain
Copy link
Contributor Author

achingbrain commented Jun 14, 2024

When implementing this I hadn't realised that libjuice will only ever allow one UDP socket to be used so unregistering the handler takes a host/port pair in order to unregister the handler only for that pair.

I hope this can be worked around as it's quite common to create two listeners during a test run and they'll not be able to co-exist in the same process like this.

We could, for example invalidate the module cache when loading the native part of node-datachannel to get a separate copy of libdatachannel that would run in it's own memory space but it's a bit of a hack.

Also if the application has already created a peer connection outside of libp2p's control, setting the host/port will have no effect as it would have already opened the port specified in the first invocation, and now we'd be setting the handler for that port which may not be what the user expects. Clarified by @xicilion below

Anyway, if you'd prefer a separate function without the host/port to remove the handler please shout.

It may not need the void *user_ptr arg to juice_bind_stun then since it can just invoke the global reference stored in unboundStunCallback.

@xicilion
Copy link
Contributor

Also if the application has already created a peer connection outside of libp2p's control, setting the host/port will have no effect as it would have already opened the port specified in the first invocation, and now we'd be setting the handler for that port which may not be what the user expects.

This should not happen. In order to avoid such hidden errors, I added a check in juice_bind_stun to prevent binding when mux connections are detected, so as to avoid binding to ports that users do not expect.

@xicilion
Copy link
Contributor

The Mux mode is bound to a unique socket, which has no problem at all in a normal WebRTC application, after all, you can't even set the port in the browser.

But in the Peer B application of WebRTC Direct, it is restricted by this. One process can only bind to one port, which greatly limits its usage.

Calls the functions added to libjuice in paullouisageneau/libjuice#248

Exports a `OnUnhandledStunRequest` function that can be passed a
callback that will be invoked when an incoming STUN message is
received that has no corresponding agent for the ICE ufrag.

Closes paullouisageneau#1166
@achingbrain achingbrain force-pushed the feat/expose-callback-for-unknown-stun-ufrag branch from 41e2380 to 311fea0 Compare January 14, 2025 16:54
@achingbrain
Copy link
Contributor Author

achingbrain commented Jan 16, 2025

Needs the changes in paullouisageneau/libjuice#292 before this can be merged.

src/global.cpp Outdated

#endif

void OnUnhandledStunRequest ([[maybe_unused]] std::string host, [[maybe_unused]] int port, [[maybe_unused]] UnhandledStunRequestCallback callback, [[maybe_unused]] void *userPtr) {
Copy link
Owner

Choose a reason for hiding this comment

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

The name should reflect that this listens only on mux mode, and that it attempts to listen to a specific port, you could rename it to ListenIceUdpMux() for instance, since the feature is called enableIceUdpMux in rtc::Configuration.

host should also be optional to allow any address (it would pass NULL to libjuice), and you could remame it to bindAddress as it is the name in the configuration. If it's not part of the mapping, I think it should be moved at the end and made an optional parameter.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed!

src/global.cpp Outdated
if (callback == NULL) {
PLOG_DEBUG << "Removing unhandled STUN request listener";

free(unboundStunCallbacks.at(port));
Copy link
Owner

Choose a reason for hiding this comment

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

This causes a race condition: If the callback is called in parallel it will cause a use after free.

Also, if no callback is set for the port it will throw an std::out_of_range exception which is confusing for the user so you should use find instead.

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've removed the map - ListenIceUdpMux now accepts a pointer to a callback and the caller of ListenIceUdpMux is responsible for it's memory management.

uint16_t remotePort;
};

RTC_CPP_EXPORT typedef std::function<void(UnhandledStunRequest request, void* userPtr)> UnhandledStunRequestCallback;
Copy link
Owner

Choose a reason for hiding this comment

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

An std::function should not have a user pointer as it has its own context. The user can use bind, lambda capture, or a callable object.

Additionally, all callbacks in the library should be implemented with synchronized_callback to prevent race conditions between calling and setting/resetting.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed the user pointer part.

I'm not sure I follow about synchronized_callback - where it's used elsewhere here a callback has been passed in that's stored as a class member. That member can get overwritten so I can see why we need to synchronize on it's access.

Here we're passing a reference to a function to libjuice and all state is passed along with the function invocation - do we still need synchronized_callback?

Comment on lines 46 to 49
struct UnhandledStunRequestHandler {
UnhandledStunRequestCallback callback;
void *userPtr;
};
Copy link
Owner

Choose a reason for hiding this comment

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

I think you don't need this since the user pointer is useless in C++.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed!

uint16_t port;
} rtcUnhandledStunRequest;

typedef void(RTC_API *rtcUnhandledStunRequestCallbackFunc)(rtcUnhandledStunRequest request);
Copy link
Owner

Choose a reason for hiding this comment

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

On the contrary, the C callback should have a user pointer as there is no way to pass context otherwise. It should be stored by capturing it in the C++ callback.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm, I think this def might be left over from a previous attempt. I've removed it.

src/global.cpp Outdated

PLOG_DEBUG << "Adding listener for unhandled STUN requests";

UnhandledStunRequestHandler *handler = (UnhandledStunRequestHandler*)calloc(1, sizeof(UnhandledStunRequestHandler));
Copy link
Owner

Choose a reason for hiding this comment

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

Please don't use C-style functions in C++. Here, you should use a unique_ptr or directly set the object as map value. However, this should not be necessary anymore if you remove the user pointer.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed!

src/global.cpp Outdated
handler->callback = std::move(callback);
handler->userPtr = userPtr;

unboundStunCallbacks[port] = handler;
Copy link
Owner

Choose a reason for hiding this comment

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

There should be a mutex protecting the map otherwise the function is not thread-safe.

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've removed the map.

src/global.cpp Outdated
@@ -88,6 +93,61 @@ std::shared_future<void> Cleanup() { return impl::Init::Instance().cleanup(); }

void SetSctpSettings(SctpSettings s) { impl::Init::Instance().setSctpSettings(std::move(s)); }

std::map<int, UnhandledStunRequestHandler *> unboundStunCallbacks;
Copy link
Owner

Choose a reason for hiding this comment

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

Should callbacks be specified by port or by bind address? Just asking because it needs to be consistent with libjuice. I think port makes sense, since the user is typically going to set the same bindAddress everytime, and the any address is quite tedious to handle.

Copy link
Contributor Author

@achingbrain achingbrain Jan 17, 2025

Choose a reason for hiding this comment

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

The libjuice PR uses ${host}:${port} as the key.

Since the conn_mux.c uses udp_socket_config_t to bind a UDP port, I wanted to make sure the user could explicitly bind both IPv4 and IPv6 addresses with:

juice_mux_listen("0.0.0.0", port, &cb, NULL);
juice_mux_listen("::", port, &cb, NULL);

I could be misreading it but I think these lines mean you need to pass one address family or the other. For example passing NULL as the bind_address will not cause it to bind to both.

Actually I'm not sure that's correct, passing NULL seems to bind all IPv4 and IPv6 interfaces.

@achingbrain achingbrain force-pushed the feat/expose-callback-for-unknown-stun-ufrag branch from 2dca4c7 to 4e0a5d8 Compare January 17, 2025 16:13
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.

3 participants