11 December 2002

About two weeks ago I gave a talk about my shell based DNS server shdns. At the time, it couldn’t reply to traffic because of an unknown error. Tridge suggested that it was because the UDP socket was “disconnected” (i.e. has no specific end point), and therefore my shell script couldn’t use write to reply to traffic.

Well, I finally got some time to look at this, and, as ever, Tridge is right. Here’s the sample programs I used to investigate this:

// Disconnected UDP socket example: this example simply reads from clients (there can be more
// than one), and returns what they said straight back to them. You'll note that we can't use
// read and write to get to the traffic, as this is not available for disconnected UDP sockets.

#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main(int argc, char *argv[]){
  int lfd;
  struct sockaddr_in servaddr;
  struct sockaddr clientaddr;
  char buf[1024];
  size_t len;
  socklen_t clen;

  // We will listen with this file descriptor
  if((lfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0){
    fprintf(stderr, "Error whilst starting to listen\n");
    exit(42);
  }

  // Define what we are listening for
  bzero(&servaddr, sizeof(servaddr));
  servaddr.sin_family = AF_INET;
  servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
  servaddr.sin_port = htons(1234);

  // Bind to the address
  if(bind(lfd, (struct sockaddr *) &servaddr, sizeof(servaddr)) < 0){
    perror("Couldn't bind");
    exit(42);
  }

  // Do stuff
  while(1){
    len = 1024;
    printf("Reading...\n");
    clen = sizeof(clientaddr);
    if((len = recvfrom(lfd, buf, len, 0, (struct sockaddr *) &clientaddr,
		       &clen)) < 0){
      perror("Socket read error");
      exit(42);
    }
    if(len == 0) break;

    // The buffer is not null terminated
    buf[len] = '\0';
    printf("Read: %s\n", buf);

    // And send it straight back
    if(sendto(lfd, buf, len, 0, &clientaddr, clen) < 0){
      perror("Socket write error");
      exit(42);
    }
  }
}

This one above is the standard socket example. It just works, because we can use the socket specific read and write calls for the data.

// Disconnected UDP socket example: this example simply waits fro traffic, and the starts
// a process to deal with the results. One process per packet, one packet per process.
// This version wont work, because the socket is not connected. In fact, cat is smart enough
// to warn us about this:
//
//           cat: write error: Transport endpoint is not connected

#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/poll.h>
#include <netinet/in.h>

int main(int argc, char *argv[]){
  int lfd;
  struct sockaddr_in servaddr;
  struct pollfd pfd;

  // We will listen with this file descriptor
  if((lfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0){
    fprintf(stderr, "Error whilst starting to listen\n");
    exit(42);
  }

  // Define what we are listening for
  bzero(&servaddr, sizeof(servaddr));
  servaddr.sin_family = AF_INET;
  servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
  servaddr.sin_port = htons(1234);

  // Bind to the address
  if(bind(lfd, (struct sockaddr *) &servaddr, sizeof(servaddr)) < 0){
    perror("Couldn't bind");
    exit(42);
  }

  // Setup the list of file descriptors we want to wait for events on
  pfd.fd = lfd;
  pfd.events = POLLIN | POLLPRI;

  // Do stuff
  while(1){
    if(poll(&pfd, 1, -1) < 0){
      perror("Waiting for new data failled");
      exit(42);
    }

    printf("Data arrived\n");

    // Spawn a child to handle this packet
    switch(fork()){
    case -1:
      perror("Couldn't spawn child to handle connection");
      exit(42);

    case 0:
      // Child process -- setup the file descriptors, and the run the helper application
      dup2(lfd, 0);
      dup2(lfd, 1);

      execl("/bin/cat", "cat", NULL);
      perror("Exec failled");
      exit(42);
      break;

    default:
      // Parent process
      break;
    }
  }
}

This one is the exec version of the first example. This is basically exactly what inetd and xinetd do. It doesn’t work, because cat doesn’t know that it has to use the socket specific read and write, instead of the normal ones.

// Connected UDP socket example: this example simply reads from clients (there can be more
// than one), and returns what they said straight back to them. You'll note that we can now use
// read and write to get to the traffic...

#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main(int argc, char *argv[]){
  int lfd;
  struct sockaddr_in servaddr;
  struct sockaddr clientaddr;
  char buf[1024];
  size_t len;
  socklen_t clen;

  // We will listen with this file descriptor
  if((lfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0){
    fprintf(stderr, "Error whilst starting to listen\n");
    exit(42);
  }

  // Define what we are listening for
  bzero(&servaddr, sizeof(servaddr));
  servaddr.sin_family = AF_INET;
  servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
  servaddr.sin_port = htons(1234);

  // Bind to the address
  if(bind(lfd, (struct sockaddr *) &servaddr, sizeof(servaddr)) < 0){
    perror("Couldn't bind");
    exit(42);
  }

  // Do stuff
  while(1){
    // We need to peek at the first part of the packet to determine who to connect to
    len = 1;
    printf("Reading...\n");
    clen = sizeof(clientaddr);
    if((len = recvfrom(lfd, buf, len, MSG_PEEK, (struct sockaddr *) &clientaddr,
		       &clen)) < 0){
      perror("Socket peek error");
      exit(42);
    }
    if(len == 0) break;

    // Connect
    if(connect(lfd, &clientaddr, clen) < 0){
      perror("Could not connect");
      exit(42);
    }

    // And now we can just use the normal read and write
    len = 1024;
    if((len = read(lfd, buf, len)) < 0){
      perror("Socket read error");
      exit(42);
    }
    if(write(lfd, buf, len) < 0){
      perror("Socket write error");
      exit(42);
    }
  }
}

Here’s an example of just the socket code, but connected, so that we can just use the read and write functions on the file descriptor (as if it was a file).

// Connected UDP socket example: this example this example simply waits fro traffic, and the
// starts a process to deal with the results. One process per packet, one packet per process.
// You'll note that we can now use read and write to get to the traffic, and that this all
// works...

#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main(int argc, char *argv[]){
  int lfd;
  struct sockaddr_in servaddr;
  struct sockaddr clientaddr;
  char buf[1024];
  size_t len;
  socklen_t clen;

  // We will listen with this file descriptor
  if((lfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0){
    fprintf(stderr, "Error whilst starting to listen\n");
    exit(42);
  }

  // Define what we are listening for
  bzero(&servaddr, sizeof(servaddr));
  servaddr.sin_family = AF_INET;
  servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
  servaddr.sin_port = htons(1234);

  // Bind to the address
  if(bind(lfd, (struct sockaddr *) &servaddr, sizeof(servaddr)) < 0){
    perror("Couldn't bind");
    exit(42);
  }

  // Do stuff
  while(1){
    // We need to peek at the first part of the packet to determine who to connect to
    len = 1;
    printf("Reading...\n");
    clen = sizeof(clientaddr);
    if((len = recvfrom(lfd, buf, len, MSG_PEEK, (struct sockaddr *) &clientaddr,
		       &clen)) < 0){
      perror("Socket peek error");
      exit(42);
    }
    if(len == 0) break;

    // Connect
    if(connect(lfd, &clientaddr, clen) < 0){
      perror("Could not connect");
      exit(42);
    }

    printf("Data arrived\n");

    // Spawn a child to handle this packet
    switch(fork()){
    case -1:
      perror("Couldn't spawn child to handle connection");
      exit(42);

    case 0:
      // Child process -- setup the file descriptors, and the run the helper application
      dup2(lfd, 0);
      dup2(lfd, 1);

      execl("/bin/cat", "cat", NULL);
      perror("Exec failled");
      exit(42);
      break;

    default:
      // Parent process
      break;
    }
  }
}

And finally, this is what inetd and xinetd should do. And now cat works as a UDP echo server…

The next step is to write up the patches to inetd and xinetd. It strikes me as being singularly useless otherwise…

Peter Morgan also seems to think I am on the right track with my honours thesis, which is a good thing.

Company social golf in the evening. Pictures up soon!