roc-toolkit icon indicating copy to clipboard operation
roc-toolkit copied to clipboard

Allow configuring outgoing port range of sender

Open gavv opened this issue 1 year ago • 0 comments

Problem

We already allow to configure outgoing sender address using roc_sender_configure() function via outgoing_address field of roc_interface_config struct.

Now we should also allow to configure outgoing port. Currently we always use zero port, which means "select random free ephemeral port".

The user may want to specify specific port or a port range, which would mean "select random free port from the range".

The typical usage of this feature will be restricting sender to ports which are open in the firewall. The user just tells us what ports Roc can use and doesn't bother with the housekeeping needed to track what ports are free.

Solution

  • Add new fields for minimum and maximum outgoing port numbers to roc_interface_config

  • Add a command-line option for port range to roc-send. Update manual pages. Add example.

  • Update node::Sender::configure()

  • Add port range to netio::UdpSenderConfig.

  • Use port range from config in netio::UdpSenderPort::open().

When a port range is given, we can't delegate the selection of a random free port to kernel and should do it by ourselves.

We could do a linear search and try all ports in range one by one until we successfully bind one. Or we could try to bind to a random port from range in a loop until bind succeeds. Both approaches are not efficient, and the second one can even cause a hang.

I suggest the following approach (but feel free to suggest something better!):

  • Add a new class netio::PortAllocator, allowing to allocate a random port from given range and then free it. We can implement it using a sparse bitmask of free and used ports. It doesn't have to be thread-safe because it will be used only from network (libuv) thread. We should cover it with unit tests.

  • Add an instance of PortAllocator to netio::NetworkLoop. Pass a reference to it to UdpSenderPort and UdpReceiverPort constructors.

  • When no port range is given, UdpSenderPort::open() and UdpReceiverPort::open() should just tell PortAllocator to mark the port as used. And when they're closed, they should mark the port as free again.

  • When port range is given, UdpSenderPort::open() should use PortAllocator to find a free port from the given range.

    Here is a possible implementation (just a pseudo-code showing the idea):

    // start port allocation session
    port_allocator.begin(from, to);
    
    // check if there are still free ports (which are not allocated
    // and are not marked used in this session)
    while (port_allocator.have_free_ports()) {
        port = port_allocator.alloc_random_port();
        if (bind(addr, port) == 0) {
            break;
        }
        // mark this port as used during this session (until end())
        port_allocator.mark_used();
    }
    
    // end port allocation session
    port_allocator.end();
    

With this approach, port allocation will be efficient if the user uses a unique port range for each context (roc_context), and thus for each netio::NetworkLoop, which I think is a reasonable requirement. If the requirement is not met, port selection wouldn't be so efficient, but at least it won't hang.

Note that alloc_random_port() does not have to be truly random, but some degree of randomization will help if the port range is also used by someone else. E.g. if the implementation of PortAllocator splits bitmask into chunks, it could randomly select a chunk, and then do a linear search inside the chunk.

If we chose this approach, we should mention its requirements in comments for roc_interface_config.

gavv avatar Sep 22 '23 21:09 gavv