Time for another look at the Dirt code.

When I got the MUD running, I noticed something odd: I could only connect using the system hostname or its IP address. Connections to localhost, 127.0.0.1, or even the host’s IPv6 address all failed:

$  telnet 192.168.0.76 6715
Trying 192.168.0.76…
Connected to 192.168.0.76.
Escape character is ‘^]’.
                A B E R M U D 
                
Game Time Elapsed: 6 minutes, 52 seconds

By what name shall I call you? ^]
telnet> q
Connection closed.
$ telnet localhost 6715
Trying ::1...
telnet: connect to address ::1: Connection refused
Trying 127.0.0.1…
telnet: connect to address 127.0.0.1: Connection refused
$ 

So it works, but seems not to be accepting connections on all interfaces.

Check what we’re actually listening on

First stop netstat (or ss):

$ netstat -lntp|grep 6715
tcp        0      0 192.168.0.76:6715       0.0.0.0:*               LISTEN      1312388/../bin/aber 
$ ss -lntp|grep 6715
LISTEN 0      5       192.168.0.76:6715       0.0.0.0:*    users:(("aberd",pid=1312388,fd=0))           
$ 
  • 192.168.0.76:6715 – the local address and port we’re bound to
  • 0.0.0.0:* – any remote address and port

So the process is bound specifically to 192.168.0.76. It won’t accept connections addressed to 127.0.0.1 or ::1. We’ll accept connections _from_ anywhere, but only if they connect to 192.168.0.76

Confirming with strace

A quick strace confirms what’s happening – we’re binding specifically to 192.168.0.76

$ strace ../bin/aberd -f 2>&1|grep bind
bind(4, {sa_family=AF_INET, sin_port=htons(6715), sin_addr=inet_addr("192.168.0.76")}, 16) = 0

The Simple fix – just bind to all interfaces

A quick grep for ‘bind’ & ‘listen’ in the code points us to s_socket.c. This has the standard socket operations you’d expect for sockets.

int make_service(short unsigned int port,
		 char               my_hostname,
		 int                my_hostnamelen,
		 struct hostent     **hp,
		 struct sockaddr_in sin)
{
  int s, i;
  struct hostent h;

  if (gethostname(my_hostname,my_hostnamelen) < 0) {
    return -1; / Error in hostname? /
  }
  if ((hp = h = gethostbyname(my_hostname)) == NULL) {
    return -2; /* Error in gethostbyname /
  }
  bzero(sin,sizeof(struct sockaddr_in));
  sin->sin_family = AF_INET;
  sin->sin_port = htons(port);
  bcopy(h->h_addr_list[0],&(sin->sin_addr),h->h_length);

  if ((s = socket(AF_INET,SOCK_STREAM,0)) < 0) {
    return -3; / Error in socket call /
  }

  / Bug fix for hanging sockets. Socket options need to be set before
   * the bind takes place or they dont do ANY good.
   /
  i = 2;
  if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &i, sizeof(i)) == -1) {
	  progerror(“setsockopt(reuseaddr)”);
  }

  if (bind(s,(struct sockaddr )sin,sizeof(sin)) < 0) {
    close(s);
    return -4; / Error in bind. /
  }
  if (listen(s,5) != 0) {
    close(s);
    return -5; / Error in listen */
  }

We call socket(2), then bind(2) to it and finally listen(2). The simplest fix here would be to simply listen on all interfaces, and that was my first fix here – just add this before we bind()

sin->sin_addr.s_addr = htonl(INADDR_ANY);

strace now shows we’re listening everywhere:

$ strace ../bin/aberd -f 2>&1 | grep bind
bind(4, {sa_family=AF_INET, sin_port=htons(6715), sin_addr=inet_addr("0.0.0.0")}, 16) = 0

After that chage we can connect via localhost, the hosts IP, hostname etc.

Why Was It Written This Way?

INADDR_ANY was around when this was written so why did MUDs want to bind to a specific hostname? I guess it was simply a different era and different ‘patterns’ were used. These days it seems logical to bind to all interfaces and implement restrictions with iptables. Back then perhaps it was more like listen on the interface that this system is known as.

You might have had a system called sionnach and that would be the hostname and assigned to a particular interface. But another interface might be assigned say sionnach-private so we wouldn’t listen on that, just the interface that corresponded to the hostname.

Time to modernise

The above code uses bcopy() which I don’t recall seeing before. Even my copy of Stevens(1993) uses memcpy. So this could could do with an update from these depreciated calls. Also it’s not accepting connections on IPv6 addresses.

A bit of reading later and the standard approach to this seems to be to use a duak-stack socket. It basically does:

  • try IPv6 first
  • allow it to accept IPv4 connections (IPV6_V6ONLY = 0)
  • fall back to IPv4 if needed

and while we’re at it we can get rid of my_hostent and s_in which are unused.

Updated function:

int make_service(short unsigned int port, char my_hostname, size_t my_hostnamelen)
{
  int s, i;
  struct sockaddr_in6 sin6;
  int ipv6_only = 0; / 0 = allow both IPv4 and IPv6 (dual-stack) /

  if (gethostname(my_hostname, my_hostnamelen) < 0) {
    return -1; / Error in hostname? /
  }

  / Try to create an IPv6 socket that also accepts IPv4 connections (dual-stack) /
  memset(&sin6, 0, sizeof(sin6));
  sin6.sin6_family = AF_INET6;
  sin6.sin6_port = htons(port);
  sin6.sin6_addr = in6addr_any; / Bind to all interfaces /

  if ((s = socket(AF_INET6, SOCK_STREAM, 0)) < 0) {
    / IPv6 not available, fall back to IPv4 /
    struct sockaddr_in sin4;
    memset(&sin4, 0, sizeof(sin4));
    sin4.sin_family = AF_INET;
    sin4.sin_port = htons(port);
    sin4.sin_addr.s_addr = htonl(INADDR_ANY);

    if ((s = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
      return -3; / Error in socket call /
    }

    / Original SO_REUSEADDR code /
    i = 1;
    if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &i, sizeof(i)) == -1) {
      progerror(“setsockopt(SO_REUSEADDR, v4)”);
    }

    if (bind(s, (struct sockaddr )&sin4, sizeof(sin4)) < 0) {
      close(s);
      return -4; /* Error in bind. /
    }
  } else {
    / IPv6 socket created - set up dual-stack to also accept IPv4 /

    / Original SO_REUSEADDR code /
    i = 1;
    if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &i, sizeof(i)) == -1) {
      progerror(“setsockopt(SO_REUSEADDR, v6)”);
    }

    / Explicitly set IPV6_V6ONLY to 0 to allow both IPv4 and IPv6 connections /
    if (setsockopt(s, IPPROTO_IPV6, IPV6_V6ONLY, &ipv6_only, sizeof(ipv6_only)) == -1) {
        / If this fails, we can still bind but it will be IPv6 only, so just continue /
    }

    if (bind(s, (struct sockaddr )&sin6, sizeof(sin6)) < 0) {
      close(s);
      return -4; /* Error in bind. /
    }
  }

  if (listen(s, 5) != 0) {
    close(s);
    return -5; / Error in listen */
  }

  return s;
}

That’s my first proper dive into IPv6. A couple of hours of RFCs, compiler errors, and man pages later… 🙂 Beej’s Guide to Network Programming was the best resource I found for this https://beej.us/guide/bgnet/html/#jumping-from-ipv4-to-ipv6 but https://www.rfc-editor.org/rfc/rfc3493.html is also worth a scan

The full changeset is at https://github.com/albertw/dirt/commit/7fcf310e4588b7eb1212161cefb2a5ef4767c5e4

I think this is portable, but if anyone runs into a problem on non-linux post an issue on github please!

Categories:computing

0 Comments