1
votes

Currently, when I send a udp packet to an unreachable destination (e.g. an unbound local port), I get an event that I can't seem to distinguish from receiving a zero-length udp packet.

I am creating the socket like this:

CFSocketContext socketContext = { 0, (__bridge void *)self, CFRetain, CFRelease, NULL };
socket = CFSocketCreate(kCFAllocatorDefault, PF_INET, SOCK_DGRAM, IPPROTO_UDP, kCFSocketDataCallBack, (CFSocketCallBack)onReceivedData, &socketContext);
NSData* localEndPointData = [[IpEndPoint ipEndPointAtUnspecifiedAddressOnPort:specifiedLocalPort] sockaddrData];
CFSocketSetAddress(socket, (__bridge CFDataRef)localEndPointData);
CFSocketConnectToAddress(socket, (__bridge CFDataRef)[remoteEndPoint sockaddrData], -1);

and receiving events like so:

void onReceivedData(CFSocketRef socket, CFSocketCallBackType type, CFDataRef address, const void *data, void *info) {
    if(type == kCFSocketDataCallBack && CFDataGetLength((CFDataRef)data) == 0) {
        NSLog("Received empty packet");
    }
}

If there is a socket listening at specifiedLocalPort then things work properly. Sending data triggers no received events, receiving data triggers a received event. If there's no socket listening at specifiedLocalPort, then sending data triggers a received event claiming an empty udp packet was received.

Am I doing something stupid, to cause this behaviour? How can I distinguish 'destination unreachable' from 'destination sent you an empty udp packet'?

1
I haven't played with sockets in Objective C, but is it possible that it's setting errno on failure? A lot of the C libraries work like that.sapi

1 Answers

0
votes

The original UNIX socket implementation had no way to communicate errors asynchronously (the whole API is synchronous). So if a socket ran into an asynchronous error (an error not a direct consequence of calling send() or recv()), it tagged itself as having data to read and when the program was trying to read the data, the read call would fail and the set error would be the asynchronous error.

In your case the error is asynchronous, as the send call did not fail. The packet was correctly sent out to the destination. Yet the destination refused to accept that packet and replied with an ICMP message, that the port is not available. This message arrives asynchronously at your host, long after your send call has already returned.

So when a socket signals you that you can read data, then there are two possibilities:

  1. There is really data available to read.
  2. An asynchronous error took place and the read call gives you the error code.

When using kCFSocketDataCallBack, you cannot distinguish these two cases, since this the API will always try to directly read data when the socket says it there is data available for reading and if that read fails, you will get an empty CFDataRef object.

See CFSocket implementation:

if (__CFSocketReadCallBackType(s) == kCFSocketDataCallBack) {
        
    // ... <snip> ...
        
    if (__CFSocketIsConnectionOriented(s)) {
        buffer = bufferArray;
        recvlen = recvfrom(s->_socket, buffer, MAX_CONNECTION_ORIENTED_DATA_SIZE, 0, (struct sockaddr *)name, (socklen_t *)&namelen);
    } else {
        buffer = malloc(MAX_DATA_SIZE);
        if (buffer) recvlen = recvfrom(s->_socket, buffer, MAX_DATA_SIZE, 0, (struct sockaddr *)name, (socklen_t *)&namelen);

    // ... <snip> ...

    if (0 >= recvlen) {
        //??? should return error if <0
        /* zero-length data is the signal for perform to invalidate */
        data = CFRetain(zeroLengthData);
    } else {
        data = CFDataCreate(CFGetAllocator(s), buffer, recvlen);
    }

Source: https://opensource.apple.com/source/CF/CF-476.18/CFSocket.c.auto.html

The line //??? should return error if <0 says it all. Yes, this should return an error, yet it just returns an empty data blob.

Instead you have to use kCFSocketReadCallBack. Now your callback is performed whenever the socket says that it has data available for reading, yet no data has been read so far. You will then have to read the data yourself using POSIX socket API:

int rawSocket = CFSocketGetNative(socket);
if (rawSocket < 0) { /* ... handle invalid socket descriptor error ... */ }

CFDataRef data = NULL;
CFErrorRef error = NULL;
for (;;) {
    char buffer[MAX_PACKET_SIZE];
    ssize_t bytesRead = read(rawSocket, &buffer, MAX_PACKET_SIZE);

    if (bytesRead < 0) {
        int errorCode = errno;
        if (errorCode == EINTR) {
            // Read call was just interrupted; that's uncritical, just try again
            continue;
        }
        error = CFErrorCreate(NULL, kCFErrorDomainPOSIX, errorCode, NULL);
        break;
    }

    data = CFDataCreate(NULL, buffer, bytesRead);
    break;
}

// Either data or error is not NULL