UDP via IPv6 (NTP, sockets)

So I wrote this NTP client. Not an NTP daemon, it just queries an NTP server once. The point was to try a bit of network programming with Zig and get more familiar with NTP. It worked out quite nicely. I think I even got the arithmetic across the NTP era change right ^^

Now here comes the “but” and the question. It’s working fine via IPv4, but fails via IPv6. The query packet, which is just one UDP datagram, gets sent but there’s no response. Here are the steps to reproduce the problem. You can find the full code in this branch.

const std = @import("std");

// make a src address. use the default adapter.
const addr_src = try std.net.Address.resolveIp("0:0:0:0:0:0:0:0", 0);

// make a socket to send from.
const sock = try std.posix.socket(
        posix.AF.INET6,
        posix.SOCK.DGRAM | posix.SOCK.CLOEXEC,
        posix.IPPROTO.UDP,
);
try posix.bind(sock, &addr_src.any, addr_src.getOsSockLen());

// obtain addresses for server to query. for this example, it will be an
// IPv6 in the list.
const addrlist = try net.getAddressList(allocator, "ntp.ubuntu.com", port);

// iterate the list, send an NTP query packet and wait for reply.
var buf: [1024]u8 = std.mem.zeroes([1024]u8);
for (addrlist.addrs) |dst| {
        var dst_addr_sock = dst.any; // std.posix.sockaddr
        var dst_addr_len: posix.socklen_t = dst.getOsSockLen();
        // make the NTP query packet and fill the bytes into send-buffer:
        ntp.Packet.toBytesBuffer(proto_vers, true, &buf);
        const n_sent = try std.posix.sendto(
            sock,
            buf[0..ntp.packet_len], // 48 bytes
            0, // no flags
            &dst_addr_sock,
            dst_addr_len,
        );
// wait for reply etc.
}

No error, the packet gets sent but: no reply. Inspecting the sent packet with Wireshark, I found that the destination address seems wrong: ntp.ubuntu.com resolves to 2620:2d:4000:1::40, but the dst address that the packet from my program has is 2620:2d:4000:1:1c00:0:a00:7b:

So the first 8 address bytes seem correct, but after that it’s nonsense? As a sanity-check, I triggered a system time sync by restarting systemd-timesyncd, and everything is as expected (packets #5439 and #5440):

Can anybody give me a hint what I’m doing wrong? Am I using the socket incorrectly maybe?

2 Likes

try to send directly to 2620:2d:4000:1::40

const addr_dst = try std.net.Address.parseIp("2620:2d:4000:1::40", 123);
try std.posix.sendto(sock, buf[0..ntp.packet_len], 0, &addr_dst.any, addr_dst.getOsSockLen());

I think I already tried that; the destination address of the created packet is the same; 2620:2d:4000:1:1c00:0:a00:7b. So the “wrong” part of the dst IP addr doesn’t seem to be random. Not sure what that tells me.

Found a solution.

(1) in the call to sendto, I have to set the pointer to the destination socket directly instead of assigning it to a variable first:

// dst is my destination address (std.net.Address)
const n_sent = try posix.sendto(
    sock,
    buf[0..ntp.packet_len],
    0,
    &dst.any, // not &dst_addr_sock !
    dst_addr_len,
);

That ensures that the destination address is correct. It is just nonsense otherwise.

(2) in the call to recvfrom, the from-address pointer must point to an undefined socket:

var dst_addr_sock: posix.sockaddr = undefined; // not dst.any !
var dst_addr_len: posix.socklen_t = dst.getOsSockLen();
const n_recv: usize = try posix.recvfrom(
    sock,
    buf[0..],
    0,
    &dst_addr_sock,
    &dst_addr_len,
);

That ensures that the returned packet is received correctly. The data came in incomplete and corrupted otherwise.

I fear I don’t actually understand why it works like this and not like my initial version. If somebody can explain this (or even just give me a hint), that would be great :slight_smile: