erlang / otp

Erlang/OTP
http://erlang.org
Apache License 2.0
11.43k stars 2.96k forks source link

gen_tcp cannot connect to IPv6 server through link-local address #4852

Closed ht-hieu closed 2 years ago

ht-hieu commented 3 years ago

Describe the bug gen_tcp get badarg when connecting to IPv6 server through link-local address. However, it can connect to a publish domain through IPv6.

To Reproduce I have two machines, the first machine opens a TCP v6 server by using netcat.

nc -6 -l 9988

The second machine open erlang shell to connect to the opened server

1> {ok,Addr} = inet_parse:ipv6_address("fe80::2631:aee8:74dd:378d%enp0s31f6").
{ok,{65152,0,0,0,9777,44776,29917,14221}}
2> gen_tcp:connect(Addr, 9988, [binary, inet6]).
** exception exit: badarg
     in function  gen_tcp:connect/4 (gen_tcp.erl, line 167)
3>

Expected behavior The erlang code can connect to the server and can transmit/receive data.

Affected versions I tried Erlang 23 on Fedora 34, Erlang 21 on raspberry pi and Erlang 24 build from source.

RoadRunnr commented 3 years ago

One Linux, when using link local IPv6 addresses, one needs to set sin6_scope_id to id of the network interface where that link local address is living.

With Erlang there are two problems with that:

ht-hieu commented 3 years ago

How can we establish a link local TCP connection using Erlang?

RaimoNiskanen commented 3 years ago

Use FreeBSD ;-)

I think it actually works there because they have Scope ED 0 for link local. Not entirely sure, though.

Or use IPv4 for link local.

Now and then I have tried to make this work on Linux, with the new socket API, where it is possible to specify an address with Scope ID. But I have still not found a reliable way, since I have not yet found out which Scope ID to use.

It would be possible, in the gen_tcp API, to pass the Scope ID buried in the link local address as FreeBSD does internally in the kernel. This restricts the Scope ID to 16 bit, so it is not a complete solution. `inet:parse_address/1 would then need to be augmented to interpret the %ScopeID suffix, and we still lack a way to translate Interface to ScopeID, and there seems to be a new concept in which the Scope ID 0x16 means link-local, bit I can not find any documentation for this.

So, anybody than can educate me / us on how to handle IPv6 ScopeID:s on Linux, please speak up!

RoadRunnr commented 3 years ago

easy, on Linux 5.11, OTP-24, without error handling

-module(test).

-export([get/2]).

get(LL, Port) ->
    [Host, If] = string:lexemes(LL, "%"),
    {ok, Idx} = net:if_name2index(If),
    {ok, IP6} = inet:parse_ipv6_address(Host),
    Addr = #{family => inet6,
         port => Port,
         addr => IP6,
         scope_id => Idx},

    io:format("Host: ~s~nInterface: ~s~nScope: ~p~n",
          [Host, If, Idx]),

    {ok, Socket} = socket:open(inet6, stream, tcp),
    ok = socket:connect(Socket, Addr, infinity),
    socket:send(Socket, "GET / HTTP/1.0\r\n\r\n"),
    {ok, Data} = socket:recv(Socket),
    io:format("Data:~n~p~n", [Data]),
    ok.
Erlang/OTP 24 [erts-12.0] [source] [64-bit] [smp:12:12] [ds:12:12:10] [async-threads:1] [jit]

Eshell V12.0  (abort with ^G)
1> c(test).                                          
{ok,test}
2> test:get("fe80::fb43:f093:7df5:dfdc%wlan0", 8000).
Host: fe80::fb43:f093:7df5:dfdc
Interface: wlan0
Scope: 5
Data:
<<"HTTP/1.1 404 Not Found\r\nServer: webfs/1.21\r\nConnection: Close\r\nAccept-Ranges: bytes\r\nContent-Type: text/plain\r\nContent-Length: 28\r\nDate: Thu, 20 May 2021 10:28:42 GMT\r\n\r\nFile or directory not found\n">>
ok
3> 
ht-hieu commented 3 years ago

Thank you for your suggestion. It works for me on Erlang 23. Sadly, gen_tcp doesn't support setting scope id.

RaimoNiskanen commented 3 years ago

net:getaddrinfo("fe80::fb43:f093:7df5:dfdc%wlan0") should give a list of addresses, one per protocol. Loop over it to find an item with type := stream and use the family := Family and first item in the protocol := Protocols list for socket:open/3.

RaimoNiskanen commented 3 years ago

@RoadRunnr: That was enlightening. Can this be made to work for the loopback interface?

RoadRunnr commented 3 years ago

@RaimoNiskanen getaddrinfo works nicely. I didn't even knew that function was there

I have no idea how to assign link local address to loopback. However, it works nicely with a dummy interface:

# ip link add dummy0 type dummy
# ip link set up dummy0
# ip -6  addr show dev dummy0
9: dummy0: <BROADCAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN group default qlen 1000
    inet6 fe80::c8f8:70ff:fe2f:ae01/64 scope link 
       valid_lft forever preferred_lft forever
2> net:getaddrinfo("fe80::c8f8:70ff:fe2f:ae01%dummy0").    
{ok,[#{addr =>
           #{addr => {65152,0,0,0,51448,28927,65071,44545},
             family => inet6,flowinfo => 0,port => 0,scope_id => 9},
       family => inet6,protocol => tcp,type => stream},
     #{addr =>
           #{addr => {65152,0,0,0,51448,28927,65071,44545},
             family => inet6,flowinfo => 0,port => 0,scope_id => 9},
       family => inet6,protocol => udp,type => dgram},
     #{addr =>
           #{addr => {65152,0,0,0,51448,28927,65071,44545},
             family => inet6,flowinfo => 0,port => 0,scope_id => 9},
       family => inet6,protocol => ip,type => raw}]}
RaimoNiskanen commented 3 years ago

I thought the idea with a loopback interface was to have an always present local interface.

20> net:getaddrinfo("::1").                          
{ok,[#{addr =>
           #{addr => {0,0,0,0,0,0,0,1},
             family => inet6,flowinfo => 0,port => 0,scope_id => 0},
       family => inet6,
       protocol => [tcp,'TCP'],
       type => stream},
     #{addr =>
           #{addr => {0,0,0,0,0,0,0,1},
             family => inet6,flowinfo => 0,port => 0,scope_id => 0},
       family => inet6,
       protocol => [udp,'UDP'],
       type => dgram},
     #{addr =>
           #{addr => {0,0,0,0,0,0,0,1},
             family => inet6,flowinfo => 0,port => 0,scope_id => 0},
       family => inet6,
       protocol => [ip,'IP'],
       type => raw}]}
21> net:getaddrinfo("::1%lo").
{error,enoname}

But I can not get it to work. getaddrinfo says scope ID is 0, the interface index is 1, and ifconfig says: scopeid 0x10<host>. I have tried all and fail to connect; I have not figured out how to bind a socket to the ::1 address with a scope ID that I can connect to.

RaimoNiskanen commented 3 years ago

net:getaddrinfo/2 takes a second argument ServiceName, so:

36> net:getaddrinfo("::1", "https"). 
{ok,[#{addr =>
           #{addr => {0,0,0,0,0,0,0,1},
             family => inet6,flowinfo => 0,port => 443,scope_id => 0},
       family => inet6,
       protocol => [tcp,'TCP'],
       type => stream}]}

But it seems we have introduced a bug in OTP-24.0 protocol := P should not be a list! And the type spec could be more precise... The intent of getaddrinfo is that it should return what you need to open a socket and connect to the service you looked up.

RaimoNiskanen commented 3 years ago

That bug in net:getaddrinfo/2 will be fixed in the upcoming OTP-24.0.2 and onwards.

bmk commented 2 years ago

A fix/update for this (link-local) has been merged into maint, and will be part of OTP 24.3.