libzmq icon indicating copy to clipboard operation
libzmq copied to clipboard

DNS addresses do not work reliably with ZMQ_IPV6 without IPv6 connectivity

Open jagerman opened this issue 4 years ago • 1 comments

Issue description

I am trying to set up zmq listeners that can accept connections over either IPv6 or IPv4, and outbound connections that connect to either IPv6 or IPv4, but this has proven harmful when DNS names are used. It seems that whenever a host has both an AAAA and A record, ZMQ_IPV6 appears to imply, for establishing a connection, "only use IPv6 when an AAAA record exists and don't even try IPv4," though this is at odds with the option's description and makes the option quite useless to enable in a context where the code might run somewhere that doesn't have IPv6 connectivity.

In particular, if A is a listening socket where either of:

  • A is not using BIND_IPV6
  • B does not have IPv6 connectivity

and B has ZMQ_IPV6 enabled then B will be unable to connect to A using a DNS hostname that resolves to both IPv6 and IPv4 addresses.

For example:

  • listener A binds to tcp://*:5555 using a socket with ZMQ_IPV6 set, and is available at example.com, which resolves to both an IPv6 and IPv4 address.
  • listener B sets up a socket with ZMQ_IPV6 set (because, according to the description, this means "the socket will connect to ... both IPv4 and IPv6 hosts") and attempts to connect to tcp://example.com:5555

If B happens to have IPv6 then the connection succeeds over IPv6 and everything is fine.

If B doesn't have IPv6 connectivity, however, the connection will never succeed.

Similarly, if A doesn't turn on ZMQ_IPV6 but has IPv6 connectivity, and B does enable ZMQ_IPV6, B will not be able to connect to A if using a DNS name that has an IPv6 address. This is particularly noticeable if A is listening on some address that should be reachable via localhost (e.g. tcp://*:5555 or tcp://127.0.0.1:5555) then B will fail to connect to tcp://localhost:5555.

This is rather unexpected behaviour and makes IPv6 support seem rather crippled.

As to why I'd want to enable ZMQ_IPV6 on a client without IPv6 connectivity: I want to distribute zmq software that accepts a configurable zmq address and can connect to IPv6 addresses, as the option description seems to imply. Failing if ever used on an IPv4-only host that tries to connect to a dual-stack hostname is not workable, however.

Environment

  • libzmq version (commit hash if unreleased): 4.3.4 (Debian package 4.3.4-1)
  • OS: Debian

Minimal test code / Steps to reproduce the issue

  1. I modified the "getting started guide" first "hello world" example to enable ZMQ_IPV6 on the socket in both server and client, ran the server on an IPv6 connected host and tried to connect to it via tcp://example.com:5555 from a host without IPv6 connectivity. The connection fails, but using a literal IPv4 address or a DNS name that only resolves to an IPv4 address succeeds.

What's the expected result?

There needs to be a way to say "use IPv6 when available", but currently ZMQ_IPV6 doesn't accomplish that.

jagerman avatar Dec 02 '21 22:12 jagerman


import zmq
import socket
import time

def connect_to_server(address):
    context = zmq.Context()
    socket = context.socket(zmq.REQ)

    try:
        print(f"Attempting to connect to {address} (IPv6)...")
        socket.connect(address)  # Try connecting with the specified address
        return socket
    except zmq.ZMQError as e:
        print(f"Failed to connect to {address} (IPv6). Error: {e}")
        print("Falling back to IPv4...")

    # Extract the hostname and port from the address
    hostname = address.split("://")[-1].split(":")[0]
    port = address.split(":")[-1]

    # Resolve the hostname to get the IPv4 address
    try:
        ipv4_address = socket.gethostbyname(hostname)  # This will return an A record
        ipv4_full_address = f"tcp://{ipv4_address}:{port}"
        print(f"Attempting to connect to {ipv4_full_address} (IPv4)...")
        socket.connect(ipv4_full_address)  # Try IPv4 address
        return socket
    except Exception as e:
        print(f"Failed to resolve or connect to IPv4 address. Error: {e}")
        return None

def run_client():
    address = "tcp://example.com:5555"  # Replace with your actual DNS name
    socket = connect_to_server(address)
    
    if socket:
        while True:
            socket.send_string("Hello from client")
            message = socket.recv_string()
            print(f"Received reply: {message}")
            time.sleep(1)

if __name__ == "__main__":
    run_client()

ljluestc avatar Oct 19 '24 18:10 ljluestc