javasocketsudpniodatagram

Why am I intermittently receiving PortUnreachableException despite bind / connect addresses matching up?


I have a small proof-of-concept where I am trying to route UDP traffic over a different port without either side being aware of the port translation. I've been able to get this working if I disable the connect() call and just do 'fire-and-forget' datagrams. Additionally, my code somewhat works even with the connect() call, but it seems to pick on specific traffic and throw a PortUnreachableException.

The actual server/client (of the traffic I'm routing) may send traffic in both directions. Some of this traffic goes through just fine, but some does not and results in the unreachable port error. I've been able to reproduce this problematic server/client behavior and I've baked that implementation into my below PoC code using a simple method called run_buggySend() that you'll see below.

I've looked at the code backwards and sideways and cannot see why the exception is being thrown. I know most people use bind() or connect() but not both, but my use case requires both because some protocols that I'm routing require very specific ports for traffic in both directions. Additionally, while I could just get rid of the connect() call to make it work, I like the idea of caching the routing information for improved performance, and do not see why it shouldn't be compatible. And then without bind(), traffic coming the other way to a hard-coded port never reaches my DatagramChannel.

I'm also wondering if this might be hitting some buggy Java code, since the port unreachable exception isn't even thrown from the more relevant / correct DatagramChannel. If I send the triggering datagram to 3000, then the 13001 channel throws the error. Likewise if I send it to 13001, the 3000 channel throws an error.

That said, I'll past the minimal reproduction code below. Thank you for any tips or help!

import java.io.IOException;
import java.math.BigInteger;
import java.net.*;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.ByteChannel;
import java.nio.charset.StandardCharsets;

public class ByteChannelUDPBridge implements Runnable
{
    private final ByteBuffer buff = ByteBuffer.allocateDirect(4096);

    private final ByteChannelUDP channelA;
    private final ByteChannelUDP channelB;

    private final int portBase = 3000;
    private final int portOffset = 10000;

    public ByteChannelUDPBridge() throws IOException
    {
        InetAddress loopback = InetAddress.getLoopbackAddress();

        channelA = new ByteChannelUDP(
            new InetSocketAddress(loopback, portBase),                      //    bind=3000
            new InetSocketAddress(loopback, portBase + 1));                 // connect=3001
        channelB = new ByteChannelUDP(
            new InetSocketAddress(loopback, portOffset + portBase + 1),     //    bind=13001
            new InetSocketAddress(loopback, portOffset + portBase));        // connect=13000
    }

    public void transfer(ByteChannel chanIn, ByteChannel chanOut) throws IOException
    {
        try
        {
            chanIn.read(buff);

            if (buff.position() > 0)
            {
                System.out.print(chanIn + " recieved " + buff.position() + " bytes: " + buff.position() + " bytes: ");

                buff.flip();
                logToHex(StandardCharsets.UTF_8.decode(buff));
                buff.rewind();
                chanOut.write(buff);
                buff.clear();
            }
        }
        catch (PortUnreachableException ex)
        {
            System.err.println(chanIn + ": " + ex);
        }
    }

    public void run()
    {
        try
        {
            while (true)
            {
                transfer(channelA, channelB); // 3000  >> B:13001 -> C:13000
                transfer(channelB, channelA); // 13001 >> B:3000  -> C:3001
                Thread.sleep(1);
            }
        }
        catch (IOException | InterruptedException ex) { ex.printStackTrace(); }
        finally
        {
            try
            {
                channelA.close();
                channelB.close();
            }
            catch (IOException dontCare) { dontCare.printStackTrace(); }
        }
    }

    public void run_buggySend()
    {
        try
        {
            DatagramSocket sock = new DatagramSocket(3001); // 13000
            InetSocketAddress recip = new InetSocketAddress(InetAddress.getLoopbackAddress(), 3000); // 13001
            byte[] buffer = new byte[]{1, 2, 3};

            while (true)
            {
                sock.send(new DatagramPacket(buffer, buffer.length, recip));
                Thread.sleep(15000);
            }
        }
        catch (IOException | InterruptedException ex)
        {
            ex.printStackTrace();
        }
    }

    public void logToHex(CharBuffer arg) { System.out.format("%x\n", new BigInteger(1, arg.toString().getBytes())); }

    public static void main(String[] args) throws Exception
    {
        ByteChannelUDPBridge listener = new ByteChannelUDPBridge();
        Thread t1 = new Thread(listener);
        t1.start();

        Thread t2 = new Thread(listener::run_buggySend);
        t2.start();
    }
}

and

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ByteChannel;
import java.nio.channels.DatagramChannel;

public class ByteChannelUDP implements ByteChannel
{
    private final DatagramChannel channel;

    private final SocketAddress bind, connect;

    public ByteChannelUDP(InetSocketAddress addrBind, InetSocketAddress addrConnect) throws IOException
    {
        bind = addrBind;
        connect = addrConnect;
        channel = DatagramChannel.open();
        channel.configureBlocking(false);
        channel.bind(bind);
        channel.connect(connect);
    }

    @Override
    public int read(ByteBuffer dst) throws IOException
    {
        //return channel.read(dst); // this also causes the unreachable exception
        int before = dst.remaining();
        SocketAddress rec = channel.receive(dst);
        return dst.remaining() - before;
    }

    @Override
    public int write(ByteBuffer src) throws IOException
    {
        //return channel.write(src); // not used unless i can get read() to work
        return channel.send(src, connect);
    }

    @Override
    public boolean isOpen() { return channel.isOpen(); }

    @Override
    public void close() throws IOException { channel.close(); }

    @Override
    public String toString()
    {
        return bind.toString() + " <> " + connect.toString();
    }
}

Solution

  • Stumbled upon what looks to be the problem and solution. While the recieve() / read() method is throwing the PortUnreachableException, it is actually the send() / write() which is triggering the error.

    The receiver on one of the sides is apparently not configured correctly and easy enough to fix, and it's thus understandable that the intermediate write would be where the error manifested.

    However, for some incomprehensible reason, Java decided it made more sense to alert the caller to an undeliverable datagram in the read() method, despite the fact that it was the write() which failed. Further, this behavior doesn't seem to be documented either. Java should have thrown the exception upon the next write or in a separate 'error polling' method, not from the reading operation which is nearly completely irrelevant to the failure of the writing operation.