diff --git a/src/LocalUI.zig b/src/LocalUI.zig index 8c68826..cf91ac7 100644 --- a/src/LocalUI.zig +++ b/src/LocalUI.zig @@ -24,6 +24,10 @@ fn main_menu_handler(_: ?*anyopaque, request: Request) Handler.Error!Response { return Response{ .arena = arena, .body = .{ .element = page.element() } }; } +const SERVICE_NAME = "_rummy._udp.local."; +const SIOCGIFCONF = 0x8912; +const SIOCGIFADDR = 0x8915; + const JoinMultiplayerGame = struct { allocator: std.mem.Allocator, discovery_thread: std.Thread, @@ -84,9 +88,8 @@ const JoinMultiplayerGame = struct { const mdns_ipv6_socket = c.mdns_socket_open_ipv6(null); defer c.mdns_socket_close(mdns_ipv6_socket); - const service_name = "rummy._udp.local."; for (&[_]c_int{ mdns_ipv4_socket, mdns_ipv6_socket }) |socket| { - switch (c.mdns_query_send(socket, c.MDNS_RECORDTYPE_PTR, service_name.ptr, service_name.len, buffer.ptr, buffer.len, 0)) { + switch (c.mdns_query_send(socket, c.MDNS_RECORDTYPE_PTR, SERVICE_NAME.ptr, SERVICE_NAME.len, buffer.ptr, buffer.len, 0)) { 0 => {}, else => |err_code| log.warn("Failed to send question; error code = {}", .{err_code}), } @@ -159,9 +162,6 @@ const JoinMultiplayerGame = struct { c.MDNS_RECORDTYPE_TXT => "TXT", else => "???", }; - if (rtype == c.MDNS_RECORDTYPE_PTR) { - log.info("{?} : {s} {s} \"{}\"", .{ std_from, entrytype_str, recordtype_str, std.zig.fmtEscapes(name) }); - } switch (rtype) { c.MDNS_RECORDTYPE_PTR => { @@ -169,6 +169,16 @@ const JoinMultiplayerGame = struct { const namestr = c.mdns_record_parse_ptr(data_ptr_opaque, data_len, record_offset, record_length, &namebuffer, namebuffer.len); log.info("{?} : {s} {s} \"{}\"", .{ std_from, entrytype_str, recordtype_str, std.zig.fmtEscapes(namestr.str[0..namestr.length]) }); }, + c.MDNS_RECORDTYPE_A => { + var std_address: std.net.Ip4Address = undefined; + _ = c.mdns_record_parse_a(data_ptr_opaque, data_len, record_offset, record_length, @ptrCast(&std_address.sa)); + log.info("{?} : {s} {s} \"{}\" -> {}", .{ std_from, entrytype_str, recordtype_str, std.zig.fmtEscapes(name), std_address }); + }, + c.MDNS_RECORDTYPE_AAAA => { + var std_address: std.net.Ip6Address = undefined; + _ = c.mdns_record_parse_aaaa(data_ptr_opaque, data_len, record_offset, record_length, @ptrCast(&std_address.sa)); + log.info("{?} : {s} {s} \"{}\" -> {}", .{ std_from, entrytype_str, recordtype_str, std.zig.fmtEscapes(name), std_address }); + }, else => {}, } return 0; @@ -213,6 +223,7 @@ const HostMultiplayerGame = struct { if (server == null) { return error.OutOfMemory; // TODO: add better error message } + errdefer c.enet_host_destroy(server); // setup mDNS discovery service @@ -315,22 +326,6 @@ const HostMultiplayerGame = struct { interface: c_int, }; - // TODO: get ipv4 and ipv6 adress of server to send out over mDNS - // const SIOCGIFADDR = 0x8915; - // var ifr: std.os.ifreq = undefined; - // ifr.ifru.addr.family = std.os.AF.INET; - // const interface_name = "wlp170s0"; - // @memset(&ifr.ifrn.name, 0); - // @memcpy(ifr.ifrn.name[0..interface_name.len], interface_name); - - // const ifr_ret_value = std.os.linux.getErrno(std.os.linux.ioctl(mdns_socket6, SIOCGIFADDR, @intFromPtr(&ifr))); - // std.log.scoped(.mdns).debug("ioctl({}, SIOCGIFADDR, {*}) = {}", .{ mdns_socket6, &ifr, ifr_ret_value }); - - // std.os.ioctl_SIOCGIFINDEX(mdns_socket6, &ifr) catch unreachable; - // const interface = ifr.ifru.ivalue; - - // const mdns_ipv6_multicast_group = std.net.Address.parseIp6("ff02::fb", c.MDNS_PORT) catch unreachable; - // var socklen = mdns_sock_addr6.getOsSockLen(); std.os.setsockopt( mdns_socket6, std.os.IPPROTO.IPV6, @@ -344,7 +339,6 @@ const HostMultiplayerGame = struct { const mdns_sock_addr6 = std.net.Address.parseIp6("::", c.MDNS_PORT) catch unreachable; var socklen = mdns_sock_addr6.getOsSockLen(); std.os.bind(mdns_socket6, &mdns_sock_addr6.any, socklen) catch unreachable; - // std.os.listen(mdns_socket6, self.kernel_backlog) catch unreachable; var mdns_listen_addr: std.net.Address = undefined; std.os.getsockname(mdns_socket, &mdns_listen_addr.any, &socklen) catch unreachable; @@ -355,12 +349,7 @@ const HostMultiplayerGame = struct { std.os.getsockname(mdns_socket6, &mdns_listen_addr6.any, &socklen) catch unreachable; const ipv6_sockaddr_string = try std.fmt.allocPrint(allocator, "{}", .{mdns_listen_addr6}); - // // const SIOCGIFNAME = 0x8910; - - // const ipv4_sockaddr: std.os.sockaddr.in = @bitCast(ifr.ifru.addr); - // // const ipv4_bytes: [4]u8 = @bitCast(ipv4_sockaddr.addr); - - const service_string = try allocator.dupe(u8, "rummy._udp.local."); + const service_string = try allocator.dupe(u8, SERVICE_NAME); var hostname_buffer: [std.os.HOST_NAME_MAX]u8 = undefined; const hostname = std.os.gethostname(&hostname_buffer) catch unreachable; @@ -404,18 +393,36 @@ const HostMultiplayerGame = struct { .ttl = 0, }); - // TODO: Add AAAA and A records to mdns service - // // DNS A that maps ".local." to the an ip address - // const mdns_a_record = c.mdns_record_t{ - // .name = .{ .str = qualified_hostname.ptr, .length = qualified_hostname.len }, - // .type = c.MDNS_RECORDTYPE_A, - // .data = .{ .a = .{ - // .addr = @bitCast(ipv4_sockaddr), - // } }, - // .rclass = 0, - // .ttl = 0, - // }; - // try mdns_records.append(mdns_a_record); + // get a list of addresses to populate A and AAAA DNS records + const addresses = @import("./multiplayer.zig").getIPAddresses(allocator) catch return error.OutOfMemory; + defer this.allocator.free(addresses); + + for (addresses) |ip_address| { + switch (ip_address) { + .ipv4 => |ipv4| { + try mdns_records.append(c.mdns_record_t{ + .name = .{ .str = qualified_hostname.ptr, .length = qualified_hostname.len }, + .type = c.MDNS_RECORDTYPE_A, + .data = .{ .a = .{ .addr = .{ + .sin_addr = @bitCast(ipv4), + } } }, + .rclass = 0, + .ttl = 0, + }); + }, + .ipv6 => |ipv6| { + try mdns_records.append(c.mdns_record_t{ + .name = .{ .str = qualified_hostname.ptr, .length = qualified_hostname.len }, + .type = c.MDNS_RECORDTYPE_AAAA, + .data = .{ .aaaa = .{ .addr = .{ + .sin6_addr = @bitCast(ipv6), + } } }, + .rclass = 0, + .ttl = 0, + }); + }, + } + } const mdns_record_slice = try mdns_records.toOwnedSlice(); errdefer allocator.free(mdns_record_slice); @@ -482,6 +489,14 @@ const HostMultiplayerGame = struct { this.allocator.destroy(this); } + const nlmsghdr = extern struct { + len: u32, + type: u16, + flags: u16, + seq: u32, + pid: u32, + }; + fn mdns_thread_main(this: *@This()) !void { const buffer = try this.allocator.alloc(u8, 2048); defer this.allocator.free(buffer); @@ -516,7 +531,8 @@ const HostMultiplayerGame = struct { .{ .fd = this.mdns_socket6, .events = std.os.POLL.IN, .revents = undefined }, }; _ = std.os.poll(&pollfds, 1000) catch break; - for (pollfds) |pollfd| { + // listen for mdns messages + for (pollfds[0..2]) |pollfd| { if (pollfd.revents & std.os.POLL.IN == 0) continue; _ = c.mdns_socket_listen(pollfd.fd, buffer.ptr, buffer.len, &mdns_thread_service_callback, this); } @@ -562,7 +578,7 @@ const HostMultiplayerGame = struct { const std_from: ?std.net.Address = if (from) |f| std.net.Address{ .any = @bitCast(f.*) } else null; - if (std.mem.eql(u8, name, DNS_SD)) { + if (std.mem.eql(u8, name, DNS_SD) and (rtype == c.MDNS_RECORDTYPE_PTR or rtype == c.MDNS_RECORDTYPE_ANY)) { const answer = c.mdns_record_t{ .name = .{ .str = name.ptr, .length = name.len }, .type = c.MDNS_RECORDTYPE_PTR, @@ -605,7 +621,7 @@ const HostMultiplayerGame = struct { log.warn("{s}:{} failed to answer query, error code = {}", .{ @src().file, @src().line, ret_val }); } } - } else if (std.mem.eql(u8, name, this.service_string)) { + } else if (std.mem.eql(u8, name, this.service_string) and (rtype == c.MDNS_RECORDTYPE_PTR or rtype == c.MDNS_RECORDTYPE_ANY)) { const answer = this.mdns_records[0]; const additional = this.mdns_records[1..]; @@ -649,8 +665,9 @@ const HostMultiplayerGame = struct { log.warn("{s}:{} failed to answer query with multicast, error code = {}", .{ @src().file, @src().line, ret_val }); } } - } else if (std.mem.eql(u8, name, this.service_instance_string)) { + } else if (std.mem.eql(u8, name, this.service_instance_string) and (rtype == c.MDNS_RECORDTYPE_SRV or rtype == c.MDNS_RECORDTYPE_ANY)) { const answer = this.mdns_records[1]; + const additional = this.mdns_records[2..]; var sendbuffer: [1024]u8 = undefined; if (rclass & c.MDNS_UNICAST_RESPONSE != 0) { @@ -667,8 +684,8 @@ const HostMultiplayerGame = struct { answer, null, 0, - null, - 0, + additional.ptr, + additional.len, ); } else { _ = c.mdns_query_answer_multicast( @@ -678,8 +695,8 @@ const HostMultiplayerGame = struct { answer, null, 0, - null, - 0, + additional.ptr, + additional.len, ); } } diff --git a/src/multiplayer.zig b/src/multiplayer.zig new file mode 100644 index 0000000..ff0fa27 --- /dev/null +++ b/src/multiplayer.zig @@ -0,0 +1,186 @@ +const IpAddress = union(enum) { + ipv4: [4]u8, + ipv6: [16]u8, + + pub fn format( + this: @This(), + comptime fmt: []const u8, + options: std.fmt.FormatOptions, + writer: anytype, + ) !void { + _ = fmt; + _ = options; + switch (this) { + .ipv4 => |ip| return std.fmt.format(writer, "{}.{}.{}.{}", .{ ip[0], ip[1], ip[2], ip[3] }), + .ipv6 => |ip| { + const sextets: [8][2]u8 = @bitCast(ip); + for (sextets, 0..) |sextet, i| { + if (i > 0) { + try std.fmt.format(writer, ":{}", .{std.fmt.fmtSliceHexLower(&sextet)}); + } else { + try std.fmt.format(writer, "{}", .{std.fmt.fmtSliceHexLower(&sextet)}); + } + } + }, + } + } +}; + +pub fn getIPAddresses(allocator: std.mem.Allocator) ![]IpAddress { + const rtnetlink_socket = try std.os.socket(std.os.AF.NETLINK, std.os.SOCK.DGRAM, std.os.linux.NETLINK.ROUTE); + defer std.os.closeSocket(rtnetlink_socket); + + const rtnetlink_addr = std.os.sockaddr.nl{ + .pid = @intCast(std.os.linux.getpid()), + .groups = 0, + }; + try std.os.bind(rtnetlink_socket, @ptrCast(&rtnetlink_addr), @sizeOf(std.os.sockaddr.nl)); + + var addresses = std.ArrayList(IpAddress).init(allocator); + defer addresses.deinit(); + + const msg_get_links = addrmsg_t{ + .hdr = std.os.linux.nlmsghdr{ + .len = @sizeOf(addrmsg_t), + .type = .RTM_GETADDR, + .flags = std.os.linux.NLM_F_REQUEST | std.os.linux.NLM_F_DUMP, + .seq = 2, + .pid = @intCast(std.os.linux.getpid()), + }, + .body = ifaddrmsg_t{ + .family = 0, + .prefixlen = 0, + .flags = 0, + .scope = 0, + .interface_index = 0, + }, + }; + const iovs = [_]std.os.iovec_const{ + .{ .iov_base = &std.mem.toBytes(msg_get_links), .iov_len = @sizeOf(addrmsg_t) }, + }; + _ = try std.os.sendmsg(rtnetlink_socket, &std.os.msghdr_const{ + .name = @ptrCast(&std.os.sockaddr.nl{ .pid = 0, .groups = 0 }), + .namelen = @sizeOf(std.os.sockaddr.nl), + .iov = &iovs, + .iovlen = iovs.len, + .control = null, + .controllen = 0, + .flags = 0, + }, 0); + + // listen for netlink messages + netlink_loop: while (true) { + var buf: [8192]u8 = undefined; + + var kernel_addr = std.os.sockaddr.nl{ .pid = 0, .groups = 0 }; + var recv_iovs = [_]std.os.iovec{ + .{ .iov_base = &buf, .iov_len = buf.len }, + }; + var recv_hdr = std.os.linux.msghdr{ + .name = @ptrCast(&kernel_addr), + .namelen = @sizeOf(std.os.sockaddr.nl), + .iov = &recv_iovs, + .iovlen = recv_iovs.len, + .control = null, + .controllen = 0, + .flags = 0, + }; + const recv_msg_len = std.os.linux.recvmsg(rtnetlink_socket, &recv_hdr, 0); + const recv_msg = buf[0..recv_msg_len]; + + var nlmsg_offset: usize = 0; + while (nlmsg_offset < recv_msg.len) { + const recv_nlmsghdr: *std.os.linux.nlmsghdr = @ptrCast(@alignCast(recv_msg[nlmsg_offset..][0..@sizeOf(std.os.linux.nlmsghdr)])); + defer nlmsg_offset += recv_nlmsghdr.len; + + const nlmsg_data = recv_msg[nlmsg_offset..][0..recv_nlmsghdr.len]; + + if (recv_nlmsghdr.type == .DONE) { + break :netlink_loop; + } else if (recv_nlmsghdr.type == .RTM_NEWADDR) { + const address_message: *ifaddrmsg_t = @ptrCast(@alignCast(nlmsg_data[@sizeOf(std.os.linux.nlmsghdr)..][0..@sizeOf(ifaddrmsg_t)])); + + switch (@as(RtScope, @enumFromInt(address_message.scope))) { + .universe, .site, .link => {}, + // ignore IPs relevant only to the current machine + .host, .nowhere => continue, + } + + var attr_offset: usize = std.mem.alignForward(usize, @sizeOf(ifaddrmsg_t) + @sizeOf(std.os.linux.nlmsghdr), @alignOf(std.os.linux.rtattr)); + while (attr_offset < nlmsg_data.len) { + const attr_hdr: std.os.linux.rtattr = @bitCast(nlmsg_data[attr_offset..][0..@sizeOf(std.os.linux.rtattr)].*); + + const attr_len_u32: u32 = @intCast(attr_hdr.len); + const attr_len_aligned = std.mem.alignForward(usize, @sizeOf(std.os.linux.rtattr) + attr_len_u32, @alignOf(std.os.linux.rtattr)); + + const attr_data_offset = std.mem.alignForward(usize, attr_offset + @sizeOf(std.os.linux.rtattr), @alignOf(std.os.linux.rtattr)); + + switch (attr_hdr.type) { + .ADDRESS => { + const attr_data = nlmsg_data[attr_data_offset..][0 .. attr_hdr.len - @sizeOf(std.os.linux.rtattr)]; + switch (address_message.family) { + std.os.linux.AF.INET => try addresses.append(.{ .ipv4 = attr_data[0..4].* }), + std.os.linux.AF.INET6 => try addresses.append(.{ .ipv6 = attr_data[0..16].* }), + else => {}, + } + }, + else => {}, + } + + attr_offset += attr_len_aligned; + } + } else if (recv_nlmsghdr.type == .ERROR) { + const nlmsgerr_t = extern struct { + @"error": c_int, + hdr: std.os.linux.nlmsghdr, + }; + const err: *nlmsgerr_t = @ptrCast(@alignCast(nlmsg_data[@sizeOf(std.os.linux.nlmsghdr)..][0..@sizeOf(nlmsgerr_t)])); + std.log.warn("errno = {}", .{err.@"error"}); + } + } + } + + return addresses.toOwnedSlice(); +} + +const IFF = struct { + const UP = 1 << 0; + const BROADCAST = 1 << 1; + const DEBUG = 1 << 2; + const LOOPBACK = 1 << 3; + const POINTOPOINT = 1 << 4; + const NOTRAILERS = 1 << 5; + const RUNNING = 1 << 6; + const NOARP = 1 << 7; + const PROMISC = 1 << 8; + const ALLMULTI = 1 << 9; + const MASTER = 1 << 10; + const SLAVE = 1 << 11; + const MULTICAST = 1 << 12; + const PORTSEL = 1 << 13; + const AUTOMEDIA = 1 << 14; + const DYNAMIC = 1 << 15; +}; + +const RtScope = enum(c_int) { + universe = 0, + site = 200, + link = 253, + host = 254, + nowhere = 255, +}; + +const ifaddrmsg_t = extern struct { + family: u8, + prefixlen: u8, + flags: u8, + scope: u8, + interface_index: c_uint, +}; + +const addrmsg_t = extern struct { + hdr: std.os.linux.nlmsghdr, + body: ifaddrmsg_t, +}; + +const std = @import("std");