Bug 248579

Summary: accept(2): IPv4 connections fail (IPv6 connections succeed) with dual IPv4/IPv6 TCP servers
Product: Base System Reporter: rec
Component: kernAssignee: freebsd-net (Nobody) <net>
Status: Closed Works As Intended    
Severity: Affects Some People CC: bz, net, rscheff, tuexen
Priority: --- Keywords: needs-qa
Version: 12.1-RELEASE   
Hardware: Any   
OS: Any   

Description rec 2020-08-10 16:55:30 UTC
While writing a TCP server designed to accept both IPv4 and IPv6 connections, I've found that the recommended way fails, but the same code runs under Linux perfectly. The failure is that IPv6 connections succeed and IPv4 connections always fail.

Way to exercise bug:
- Compile code below (cc -o foo foo.c will work)
- run command ("./foo") in one window
- In another window try to connect to port 5000
  for example: telnet ::1 5000 <-- should work
               telnet 127.0.0.1 5000 <-- should work but fails 100% of the time.

Repeating this test under modern Linux results in both telnet sessions connecting.

Code to exercise bug:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <err.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int
make_listen(char *port)
{
  struct addrinfo hints, *res,	*res0;
  int error;
  int s;
  const char *cause = NULL;
  
  memset(&hints, 0, sizeof(hints));
  //  hints.ai_family = PF_UNSPEC;
  hints.ai_family = AF_INET6;  
  hints.ai_socktype = SOCK_STREAM;
  hints.ai_protocol = IPPROTO_TCP;
  hints.ai_flags = AI_PASSIVE;
  
  if ((error = getaddrinfo(NULL, port, &hints, &res0)))
    errx(1, "%s", gai_strerror(error));

  for (res = res0; res; res = res->ai_next) {
    printf("Trying Family=%d, Socktype=%d, Protocol=%d\n",
	   res->ai_family, 
	   res->ai_socktype,
	   res->ai_protocol);
    if ((s = socket(res->ai_family, 
		    res->ai_socktype,
		    res->ai_protocol)) < 0) {
      cause = "socket";
      continue;
    }
    int yes = 1;
    if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)) < 0)
      errx(1,"Setsockopt(SO_REUSEADDR) failed.");
    
    if (bind(s,
	     res->ai_addr,
	     res->ai_addrlen) < 0) {
      cause = "bind";
      close(s);
      continue;
    }
    listen(s, 5);
    freeaddrinfo(res0);
    return s;
  }
  err(1, "%s",	cause);
  freeaddrinfo(res0);
  return -1;
}

int
main(int argc, char *argv[])
{
  int listen_fd = make_listen("5000");

  while (1) {
    char addr[80];
    struct sockaddr_storage client_addr;
    socklen_t client_addr_len = sizeof(client_addr);

    int fc = accept(listen_fd,
		    (struct sockaddr *)&client_addr,
		    &client_addr_len);
    if (fc < 0) {
      warn("Accept()");
      continue;
    }

    if (client_addr.ss_family == AF_INET) {
      struct sockaddr_in *p = (struct sockaddr_in *)&client_addr;
      inet_ntop(p->sin_family, &p->sin_addr,
	      addr,sizeof(addr));
    } else {
      struct sockaddr_in6 *p = (struct sockaddr_in6 *)&client_addr;
      inet_ntop(p->sin6_family, &p->sin6_addr,
	      addr,sizeof(addr));
    }
    printf("Connection from (%d) %s\n",
	   client_addr_len,
	   addr);
    (void)write(fc,"Hello\n",6);
    close(fc);
  }
}
Comment 1 rec 2020-08-10 16:56:53 UTC
I'll gladly admit to doing something stupid, but I can't see it. Seems odd that FreeBSD would have a problem here, so I have to assume I'm the one making the error.
Comment 2 Bjoern A. Zeeb freebsd_committer freebsd_triage 2020-08-10 17:29:12 UTC
(In reply to rec from comment #1)

try (as root):

sysctl net.inet6.ip6.v6only=0

if your code then works you need to add

ipv6_ipv4mapping="YES"

into rc.conf


Please report back to let us know if the issue persists or this can be closed.
Comment 3 Michael Tuexen freebsd_committer freebsd_triage 2020-08-11 05:43:40 UTC
(In reply to rec from comment #1)
Instead of changing a system wide setting you can also use the IPPROTO_IPV6-level socket option IPV6_V6ONLY to allow handling IPv4 traffic using the IPv6 socket by calling in make_listen():

int no = 0;
setsockopt(s, IPPROTO_IPV6, IPV6_V6ONLY, no, sizeof(int));
Comment 4 Michael Tuexen freebsd_committer freebsd_triage 2020-08-11 05:44:28 UTC
(In reply to Michael Tuexen from comment #3)
I meant:
setsockopt(s, IPPROTO_IPV6, IPV6_V6ONLY, &no, sizeof(no));
Comment 5 Richard Scheffenegger freebsd_committer freebsd_triage 2020-08-12 21:45:18 UTC
Just validated the suggestion from tuexen@:

freebsd ~]# ./foo
Trying Family=28, Socktype=1, Protocol=6
Connection from (28) ::1
Connection from (28) ::ffff:127.0.0.1

freebsd ~]# nc -6 ::1 5000
Hello
^C
freebsd ~]# nc -4 127.0.0.1 5000
Hello
^C

freebsd ~]# netstat -na
Active Internet connections (including servers)
Proto Recv-Q Send-Q Local Address          Foreign Address        (state)
:
:
tcp46      0      0 *.5000                 *.*                    LISTEN
:
:


=> works as designed (different defaults than Linux). Shall this bug be retired?