How to connect to a IPv6 link local address discovered using IPv4 and set the correct network interface?

Viewed 198

Our Delphi/Indy application discovers devices using mDNS on IPv4 but some of the addresses found are link-local IPv6 addresses (specified in AAAA records along with other A records). We want to provide the option of using either IPv4 or IPv6 addresses to connect to the devices because some people insist on setting static IPv4 addresses which due to a change in location or configuration become incompatible with the subnet where they are located.

But when the application attempts to establish a TCP connection to one of these link-local IPv6 addresses, the first attempt usually fails because Windows doesn't know which interface to use. (This same behaviour can be observed when just ping-ing the address from the command line. The problem is solved by writing % followed by the appropriate zone ID at the end of the link-local address.)

The TIdTCPClient object being used to connect to the device therefore needs to be associated with the correct network interface. I understand that this can be done by setting the BoundIP property but I do not have the IPv6 address of the interface where the device was discovered because this was done using IPv4. Is there a way of using the TIdSocketHandle object that was provided in the OnUDPRead event when the device was discovered and using that to set the appropriate thing in the TIdTCPClient object?

2 Answers

But when the application attempts to establish a TCP connection to one of these link-local IPv6 addresses, the first attempt usually fails because Windows doesn't know which interface to use.

Are you setting the TIdTCPClient.IPVersion property to Id_IPv6? Indy does not currently determine the IP version to connect with based on the IP address specified (ticket), you have to explicitly specify the IP version up front.

Most of the time, it should be good enough to let Windows decide which interface to use when connecting to an IP address. That is what its routing tables are meant for.

This same behaviour can be observed when just ping-ing the address from the command line. The problem is solved by writing % followed by the appropriate zone ID at the end of the link-local address.

Indy does not currently support zone/scope IDs for IPv6 addresses (ticket).

Is there a way of using the TIdSocketHandle object that was provided in the OnUDPRead event when the device was discovered

Not likely, because you said the discovery was made over IPv4, which means the TIdSocketHandle's UDP socket was bound to an IPv4 interface, not an IPv6 interface. The only possibility would be if the interface in question has both IPv4 and IPv6 addresses assigned to it, in which case you could use Win32 APIs to match up the TIdSocketHandle.IP to a specific interface, and then enumerate the IPv6 addresses on that same interface.

Otherwise, you can use Indy's TIdStack.GetLocalAddressList() method to get all of the local IPv6 interface IPs, and then bind the TIdTCPClient to each one in a loop until a connection to the target device is successful.

Taking Remy's advice, I implemented the following:

uses Winapi.IpTypes, IdGlobal, IdWinsock2, IdStackBSDBase;

function GetAdaptersAddresses(Family: ULONG; Flags: DWORD; Reserved: PVOID; pAdapterAddresses: PIP_ADAPTER_ADDRESSES; var OutBufLen: ULONG): DWORD; stdcall; external 'iphlpapi.dll';

// Given an IPv4 address of a network interface, this returns one of the IPv6 addresses of the same interface.
function GetAdapterIPv6Address (IPv4Address : String) : String;
const
    flags = GAA_FLAG_SKIP_ANYCAST or GAA_FLAG_SKIP_MULTICAST or GAA_FLAG_SKIP_DNS_SERVER or GAA_FLAG_SKIP_FRIENDLY_NAME;
var
    BufLen : ULONG;
    Ret : DWORD;
    Adapter, Adapters : PIP_ADAPTER_ADDRESSES;
    UnicastAddr : PIP_ADAPTER_UNICAST_ADDRESS;
    UnicastAddr6 : PIP_ADAPTER_UNICAST_ADDRESS;
    Found : Boolean;
    i : Integer;
    a : UInt32;
begin
    result := '';
    a := IPv4ToUInt32(IPv4Address);
    BufLen := 0;
    Adapters := nil;
    Ret := GetAdaptersAddresses(PF_UNSPEC, flags, nil, nil, BufLen);
    if (Ret = ERROR_INSUFFICIENT_BUFFER) or (Ret = ERROR_BUFFER_OVERFLOW) then begin
        Adapters := AllocMem(BufLen);
        try
            Ret := GetAdaptersAddresses(PF_UNSPEC, flags, nil, Adapters, BufLen);
            if Ret = ERROR_SUCCESS then begin
                Adapter := Adapters;
                repeat
                    UnicastAddr6 := nil;
                    Found := false;
                    if (Adapter.IfType <> 24 {IF_TYPE_SOFTWARE_LOOPBACK}) and ((Adapter.Flags and IP_ADAPTER_RECEIVE_ONLY) = 0) then begin
                        UnicastAddr := Adapter^.FirstUnicastAddress;
                        while UnicastAddr <> nil do begin
                            if UnicastAddr^.DadState = IpDadStatePreferred then begin
                                case UnicastAddr^.Address.lpSockaddr.sin_family of
                                    AF_INET : begin
                                        with TIdIn4Addr(PSockAddrIn(UnicastAddr^.Address.lpSockaddr)^.sin_addr) do
                                            if a = ((S_un_b.s_b1 shl 24) or (S_un_b.s_b2 shl 16) or (S_un_b.s_b3 shl 8) or S_un_b.s_b4) then
                                                Found := true;
                                    end;
                                    AF_INET6 :
                                        if UnicastAddr6 = nil then
                                            UnicastAddr6 := UnicastAddr;
                                end;
                            end;
                            UnicastAddr := UnicastAddr^.Next;
                        end;
                    end;
                    Adapter := Adapter.Next;
                until (Adapter = nil) or found;
                if found and assigned(UnicastAddr6) then begin
                    for i := 0 to 7 do begin
                        if i > 0 then
                            result := result + ':';
                        with TIdIn6Addr(PSockAddrIn6(UnicastAddr6^.Address.lpSockaddr)^.sin6_addr) do
                            result := result + IntToHex(ntohs(s6_addr16[i]), 1);
                    end;
                end;
            end;
        finally
            FreeMem(Adapters);
        end;
    end;
end;

Due to the device discovery being done via IPv4, the IPv4 address of the network interface is known (IP property of TIdSocketHandle) so by using the above function, TIdTCPClient.BoundIP can be set so that the network interface is unambiguous for any link-local IPv6 address found in the discovery process.

Related