[MLton] Socket.connectNB on NetBSD

Stephen Weeks MLton@mlton.org
Fri, 28 Nov 2003 13:52:12 -0800


> This succeeds on NetBSD but fails on the university's Redhat 7.3
> boxen. The errno is set at EINPROGRESS (115 in Linux). I agree it
> should fail. Now for the fun part:
>
> Adding a line which sets sa2.sin_addr.s_addr = INADDR_LOOPBACK gives
> the intended failure behaviour on NetBSD.

I see the same behavior.  But on my SunOS machine, I still get 0.

Here are some more test programs to figure out what's going on.

Program 1 is the original which binds to port 0 with an unspecified
  internet address.
Program 2 drops the listen from program 1.
Program 3 is like 1, but specifies the internet address as 127.0.0.1.
Program 4 drops the listen from program 3.

----------------------------------------------------------------------
(* Program 1 *)
val socket = INetSock.TCP.socket ()
val _ = Socket.bind (socket, INetSock.any 0)
val _ = Socket.listen (socket, 5)
val _ =
   print (concat
	  [Bool.toString (Socket.connectNB (INetSock.TCP.socket (),
					    Socket.Ctl.getSockName socket)),
	   "\n"])

(* Program 2 *)
val socket = INetSock.TCP.socket ()
val _ = Socket.bind (socket, INetSock.any 0)
val _ =
   print (concat
	  [Bool.toString (Socket.connectNB (INetSock.TCP.socket (),
					    Socket.Ctl.getSockName socket)),
	   "\n"])

(* Program 3 *)
val socket = INetSock.TCP.socket ()
val _ = Socket.bind (socket,
		     INetSock.toAddr (valOf (NetHostDB.fromString "127.0.0.1"),
				      0))
val _ = Socket.listen (socket, 5)
val _ =
   print (concat
	  [Bool.toString (Socket.connectNB (INetSock.TCP.socket (),
					    Socket.Ctl.getSockName socket)),
	   "\n"])

(* Program 4 *)
val socket = INetSock.TCP.socket ()
val _ = Socket.bind (socket,
		     INetSock.toAddr (valOf (NetHostDB.fromString "127.0.0.1"),
				      0))
val _ =
   print (concat
	  [Bool.toString (Socket.connectNB (INetSock.TCP.socket (),
					    Socket.Ctl.getSockName socket)),
	   "\n"])
----------------------------------------------------------------------

Here are the results of running the programs on the three platforms.

	prog 1	prog 2	prog 3	prog 4
Linux	false	false	false	false
NetBSD	true	false	true	false
SunOS	true	error	true	error

The error was

	unhandled exception: SysErr: Connection refused [<UNKNOWN>]

Columns 1 and 3 are the same, which shows that it doesn't matter
whether the address is unspecified or is 127.0.0.1.  The data also
shows that as long as the listen has happened, then the connectNB
succeeds.  If the listen hasn't happened, then the connect fails,
but in different ways.  On Linux and NetBSD, the connect fails with
EINPROGRESS, while on SunOS the connect fails with ECONNREFUSED.  I
guess both of those are allowable socket behavior.

So, my conjecture as to what's going on is that the socket
implementation on NetBSD and SunOS are "optimizing" the connect even
though there hasn't been an accept yet on the other end.  That is,
because the listen has happened, they go ahead and create the socket
pair.  They then arrange for the first accept to get the appropriate
end of the pair.

The following program shows that this is indeed hapenning.

----------------------------------------------------------------------
val socket = INetSock.TCP.socket ()
val _ = Socket.bind (socket, INetSock.any 0)
val _ = Socket.listen (socket, 5)
val socket' = INetSock.TCP.socket ()
val b = Socket.connectNB (socket', Socket.Ctl.getSockName socket)
val _ =
   if b
      then
	 let
	    val (socket'', _) = Socket.accept socket
	    fun msg (name, s) = 
	       let
		  fun z a =
		     let
			val (in_addr, port) = INetSock.fromAddr a
		     in
			concat [NetHostDB.toString in_addr,
				":", Int.toString port]
		     end
	       in
		  print (concat [name, " ",
				 z (Socket.Ctl.getSockName s), " ",
				 z (Socket.Ctl.getPeerName s), "\n"])
	       end
	    val _ = msg ("socket'", socket')
	    val _ = msg ("socket''", socket'')
	 in
	    ()
	 end
   else 
      print "OK\n"
----------------------------------------------------------------------

Here is the output of the program on the three platforms.

Linux
	OK
NetBSD
	socket' 127.0.0.1:65365 127.0.0.1:65366
	socket'' 127.0.0.1:65366 127.0.0.1:65365
SunOS
	socket' 127.0.0.1:32833 127.0.0.1:32832
	socket'' 127.0.0.1:32832 127.0.0.1:32833

NetBSD and SunOS can do this optimization when both endpoints are on
the same machine.  As the following program shows, all three platforms
perform the same when doing a connectNB to another machine that isn't
listening.

------------------------------------------------------------
val _ =
   print (concat
	  [Bool.toString
	   (Socket.connectNB
	    (INetSock.TCP.socket (),
	     INetSock.toAddr (NetHostDB.addr
			      (valOf (NetHostDB.getByName "www.mlton.org")),
			      4000))),
	   "\n"])
------------------------------------------------------------

This program prints false on Linux, NetBSD, and SunOS.


My conclusion is that we have encountered a difference in socket
behavior due to an optimization performed by NetBSD and SunOS but not
by Linux.  I don't see any reason why we shouldn't expose this
difference in behavior.  So, I'm going to remove the test from our
socket regression.