From a6fe88b65e28ada2cda58097ea99fe3c4f9e6b70 Mon Sep 17 00:00:00 2001 From: geemili Date: Sun, 3 Mar 2024 00:45:06 -0700 Subject: [PATCH] start implementing mDNS service discovery for multiplayer --- build.zig | 12 ++ build.zig.zon | 8 + src/LocalUI.zig | 529 ++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 532 insertions(+), 17 deletions(-) diff --git a/build.zig b/build.zig index 52306c7..a954820 100644 --- a/build.zig +++ b/build.zig @@ -20,6 +20,16 @@ pub fn build(b: *std.Build) void { .optimize = optimize, }); + const mdns = b.dependency("mdns", .{ + .target = target, + .optimize = optimize, + }); + + const enet = b.dependency("enet", .{ + .target = target, + .optimize = optimize, + }); + const exe = b.addExecutable(.{ .name = "seizer-rummy", .root_source_file = .{ .path = "src/main.zig" }, @@ -27,6 +37,8 @@ pub fn build(b: *std.Build) void { .optimize = optimize, }); exe.root_module.addImport("seizer", seizer.module("seizer")); + exe.linkLibrary(mdns.artifact("mdns")); + exe.linkLibrary(enet.artifact("enet")); b.installArtifact(exe); const run_cmd = b.addRunArtifact(exe); diff --git a/build.zig.zon b/build.zig.zon index 441d4db..e427a2e 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -19,6 +19,14 @@ .url = "https://github.com/leroycep/seizer/archive/8fdc6335641614c1cd844d1ecd5ad937584a443e.tar.gz", .hash = "1220b6f9d0aba788b55a3e26a34ca9793495fbdb062c463cef1433ff937b96aa77d6", }, + .mdns = .{ + .url = "https://github.com/leroycep/mdns/archive/ab55ee5a1e3f72d872521b89fd28a771a1271e2e.tar.gz", + .hash = "12209523b5d87d2cb4168950c68e7a8f14b1c74001b6d9a79d35286f3631ef5ea210", + }, + .enet = .{ + .url = "https://github.com/leroycep/enet/archive/624ca683074acead6f96785d9133e074aae59574.tar.gz", + .hash = "1220f45c8055a48c87a17b16adb28531072ebf336756b8847d489076ffb55b08e3f0", + }, }, .paths = .{ // This makes *all* files, recursively, included in this package. It is generally diff --git a/src/LocalUI.zig b/src/LocalUI.zig index 4b63b97..31e7884 100644 --- a/src/LocalUI.zig +++ b/src/LocalUI.zig @@ -12,7 +12,7 @@ fn main_menu_handler(_: ?*anyopaque, request: Request) Handler.Error!Response { var join_multiplayer_link = try Element.Link.create(arena.allocator(), join_multiplayer_text.element(), .{ .handler = join_multiplayer_game }); var host_multiplayer_text = try Element.Text.create(arena.allocator(), "Host Multiplayer Game"); - var host_multiplayer_link = try Element.Link.create(arena.allocator(), host_multiplayer_text.element(), .{ .handler = host_multiplayer_game }); + var host_multiplayer_link = try Element.Link.create(arena.allocator(), host_multiplayer_text.element(), .{ .handler = HostMultiplayerGame.INTERFACE }); var play_game_hbox = try Element.HBox.create(arena.allocator()); try play_game_hbox.addElement(join_multiplayer_link.element()); @@ -42,28 +42,523 @@ fn join_multiplayer_game_handler(_: ?*anyopaque, request: Request) Handler.Error return Response{ .arena = arena, .body = .{ .element = page.element() } }; } -pub const host_multiplayer_game = &Handler.Interface{ - .create = null, - .handle = &host_multiplayer_game_handler, - .deinit = null, +const HostMultiplayerGame = struct { + allocator: std.mem.Allocator, + mdns_addr4_string: []u8, + mdns_addr6_string: []u8, + + mdns_socket4: std.os.socket_t, + mdns_socket6: std.os.socket_t, + + service_string: []const u8, + service_instance_string: []const u8, + qualified_hostname: []const u8, + mdns_records: []c.mdns_record_t, + mdns_thread: std.Thread, + mdns_thread_should_stop: std.atomic.Value(bool), + + address: c.ENetAddress, + server: *c.ENetHost, + + pub const INTERFACE = &Handler.Interface{ + .create = &create, + .handle = &handler, + .deinit = &deinit, + }; + + fn create(allocator: std.mem.Allocator) Handler.Error!?*anyopaque { + const this = try allocator.create(@This()); + errdefer allocator.destroy(this); + + // setup ENet game server + const address = c.ENetAddress{ + .host = c.ENET_HOST_ANY, + .port = 5711, + }; + + const server = c.enet_host_create(&address, 4, 2, 0, 0); + if (server == null) { + return error.OutOfMemory; // TODO: add better error message + } + + // setup mDNS discovery service + + const mdns_socket = std.os.socket(std.os.AF.INET, std.os.SOCK.DGRAM, std.os.IPPROTO.UDP) catch |e| { + std.log.scoped(.mdns).err("failed to open mdns socket = {}", .{e}); + return error.OutOfMemory; + }; + + std.os.setsockopt( + mdns_socket, + std.os.SOL.SOCKET, + std.os.SO.REUSEADDR, + &std.mem.toBytes(@as(c_int, 1)), + ) catch unreachable; + std.os.setsockopt( + mdns_socket, + std.os.SOL.SOCKET, + std.os.SO.REUSEPORT, + &std.mem.toBytes(@as(c_int, 1)), + ) catch unreachable; + std.os.setsockopt( + mdns_socket, + std.os.IPPROTO.IP, + std.os.linux.IP.MULTICAST_TTL, + &std.mem.toBytes(@as(c_int, 1)), + ) catch unreachable; + std.os.setsockopt( + mdns_socket, + std.os.IPPROTO.IP, + std.os.linux.IP.MULTICAST_LOOP, + &std.mem.toBytes(@as(c_int, 1)), + ) catch unreachable; + + const ip_mreq_t = extern struct { + multicast_address: [4]u8, + address: [4]u8, + }; + std.os.setsockopt( + mdns_socket, + std.os.IPPROTO.IP, + std.os.linux.IP.ADD_MEMBERSHIP, + &std.mem.toBytes(ip_mreq_t{ + .multicast_address = .{ 224, 0, 0, 251 }, + .address = .{ 0, 0, 0, 0 }, + }), + ) catch unreachable; + + const mdns_sock_addr = std.net.Address.parseIp4("0.0.0.0", c.MDNS_PORT) catch unreachable; + std.os.bind(mdns_socket, @ptrCast(&mdns_sock_addr), mdns_sock_addr.getOsSockLen()) catch |e| { + std.log.scoped(.mdns).err("bind failed = {}", .{e}); + return error.OutOfMemory; + }; + + std.os.setsockopt( + mdns_socket, + std.os.IPPROTO.IP, + std.os.linux.IP.MULTICAST_IF, + &std.mem.toBytes(mdns_sock_addr.any), + ) catch unreachable; + + // ipv6 socket + const mdns_socket6 = std.os.socket(std.os.AF.INET6, std.os.SOCK.DGRAM, std.os.IPPROTO.UDP) catch |e| { + std.log.scoped(.mdns).err("failed to open mdns socket = {}", .{e}); + return error.OutOfMemory; + }; + + std.os.setsockopt( + mdns_socket6, + std.os.SOL.SOCKET, + std.os.SO.REUSEADDR, + &std.mem.toBytes(@as(c_int, 1)), + ) catch unreachable; + std.os.setsockopt( + mdns_socket6, + std.os.SOL.SOCKET, + std.os.SO.REUSEPORT, + &std.mem.toBytes(@as(c_int, 1)), + ) catch unreachable; + std.os.setsockopt( + mdns_socket6, + std.os.IPPROTO.IPV6, + std.os.linux.IPV6.MULTICAST_HOPS, + &std.mem.toBytes(@as(c_int, 1)), + ) catch unreachable; + std.os.setsockopt( + mdns_socket6, + std.os.IPPROTO.IPV6, + std.os.linux.IPV6.MULTICAST_LOOP, + &std.mem.toBytes(@as(c_int, 1)), + ) catch unreachable; + std.os.setsockopt( + mdns_socket6, + std.os.IPPROTO.IPV6, + std.os.linux.IPV6.MULTICAST_IF, + &std.mem.toBytes(@as(c_int, 0)), + ) catch unreachable; + + const ipv6_mreq_t = extern struct { + addr: [16]u8, + 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, + std.os.linux.IPV6.ADD_MEMBERSHIP, + &std.mem.toBytes(ipv6_mreq_t{ + .addr = .{ 0xFF, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFB }, + .interface = 0, + }), + ) catch unreachable; + + 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; + const ipv4_sockaddr_string = try std.fmt.allocPrint(allocator, "{}", .{mdns_listen_addr}); + + socklen = mdns_sock_addr6.getOsSockLen(); + var mdns_listen_addr6: std.net.Address = undefined; + 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."); + + var hostname_buffer: [std.os.HOST_NAME_MAX]u8 = undefined; + const hostname = std.os.gethostname(&hostname_buffer) catch unreachable; + + // ".<_service-name>._udp.local." string + const service_instance_string = try std.fmt.allocPrint(allocator, "{s}.{s}", .{ + hostname, + service_string, + }); + errdefer allocator.free(service_string); + + const qualified_hostname = try std.fmt.allocPrint(allocator, "{s}.local.", .{hostname}); + errdefer allocator.free(qualified_hostname); + std.log.scoped(.mdns).info("qualified hostname = \"{}\"", .{std.zig.fmtEscapes(qualified_hostname)}); + + // Create DNS records + + var mdns_records = std.ArrayList(c.mdns_record_t).init(allocator); + defer mdns_records.deinit(); + + // DNS PTR that maps "._udp.local." to ".._udp.local." + try mdns_records.append(c.mdns_record_t{ + .name = .{ .str = service_string.ptr, .length = service_string.len }, + .type = c.MDNS_RECORDTYPE_PTR, + .data = .{ .ptr = .{ .name = .{ .str = service_instance_string.ptr, .length = service_instance_string.len } } }, + .rclass = 0, + .ttl = 0, + }); + + // DNS SRV that maps ".._udp.local." to ".local.:" + try mdns_records.append(c.mdns_record_t{ + .name = .{ .str = service_instance_string.ptr, .length = service_instance_string.len }, + .type = c.MDNS_RECORDTYPE_SRV, + .data = .{ .srv = .{ + .name = .{ .str = qualified_hostname.ptr, .length = qualified_hostname.len }, + .port = address.port, + .priority = 0, + .weight = 0, + } }, + .rclass = 0, + .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); + + const mdns_record_slice = try mdns_records.toOwnedSlice(); + errdefer allocator.free(mdns_record_slice); + + this.* = .{ + .allocator = allocator, + .mdns_addr4_string = ipv4_sockaddr_string, + .mdns_addr6_string = ipv6_sockaddr_string, + .mdns_socket4 = mdns_socket, + .mdns_socket6 = mdns_socket6, + .service_string = service_string, + .service_instance_string = service_instance_string, + .qualified_hostname = qualified_hostname, + .mdns_records = mdns_record_slice, + .mdns_thread = undefined, + .mdns_thread_should_stop = std.atomic.Value(bool).init(false), + + .address = address, + .server = server, + }; + + this.mdns_thread = std.Thread.spawn(.{}, mdns_thread_main, .{this}) catch return error.OutOfMemory; + + return this; + } + + fn handler(pointer: ?*anyopaque, request: Request) Handler.Error!Response { + const this: *@This() = @ptrCast(@alignCast(pointer)); + + var arena = std.heap.ArenaAllocator.init(request.allocator); + errdefer arena.deinit(); + + // var text = try Element.Text.create(arena.allocator(), try std.fmt.allocPrint(arena.allocator(), "Hosting Multiplayer Game at {}:{}", .{ this.address.host, this.address.port })); + var mdns_addr4_string_text = try Element.Text.create(request.allocator, this.mdns_addr4_string); + var mdns_addr6_string_text = try Element.Text.create(request.allocator, this.mdns_addr6_string); + var service_instance_string_text = try Element.Text.create(request.allocator, this.service_instance_string); + var qualified_hostname_text = try Element.Text.create(request.allocator, this.qualified_hostname); + + var page = try Element.Page.create(request.allocator); + // try page.addElement(.{ 0.5, 0.5 }, .{ 0.5, 0.5 }, text.element()); + try page.addElement(.{ 0.3, 0.6 }, .{ 0.5, 0.5 }, mdns_addr4_string_text.element()); + try page.addElement(.{ 0.7, 0.6 }, .{ 0.5, 0.5 }, mdns_addr6_string_text.element()); + try page.addElement(.{ 0.5, 0.7 }, .{ 0.5, 0.5 }, service_instance_string_text.element()); + try page.addElement(.{ 0.5, 0.9 }, .{ 0.5, 0.5 }, qualified_hostname_text.element()); + + return Response{ .arena = arena, .body = .{ .element = page.element() } }; + } + + fn deinit(pointer: ?*anyopaque) void { + const this: *@This() = @ptrCast(@alignCast(pointer)); + + c.enet_host_destroy(this.server); + + this.mdns_thread_should_stop.store(true, .Monotonic); + this.mdns_thread.join(); + + this.allocator.free(this.mdns_records); + this.allocator.free(this.mdns_addr4_string); + this.allocator.free(this.mdns_addr6_string); + this.allocator.free(this.service_string); + this.allocator.free(this.service_instance_string); + this.allocator.free(this.qualified_hostname); + + this.allocator.destroy(this); + } + + fn mdns_thread_main(this: *@This()) !void { + const buffer = try this.allocator.alloc(u8, 2048); + defer this.allocator.free(buffer); + + const log = std.log.scoped(.mdns); + log.info("mdns service started", .{}); + + _ = c.mdns_announce_multicast( + this.mdns_socket4, + buffer.ptr, + buffer.len, + this.mdns_records[0], + null, + 0, + this.mdns_records[1..].ptr, + this.mdns_records[1..].len, + ); + _ = c.mdns_announce_multicast( + this.mdns_socket6, + buffer.ptr, + buffer.len, + this.mdns_records[0], + null, + 0, + this.mdns_records[1..].ptr, + this.mdns_records[1..].len, + ); + + while (!this.mdns_thread_should_stop.load(.Monotonic)) { + var pollfds = [_]std.os.pollfd{ + .{ .fd = this.mdns_socket4, .events = std.os.POLL.IN, .revents = undefined }, + .{ .fd = this.mdns_socket6, .events = std.os.POLL.IN, .revents = undefined }, + }; + _ = std.os.poll(&pollfds, 1000) catch break; + for (pollfds) |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); + } + } + + log.info("mdns service stopping", .{}); + } + + fn mdns_thread_service_callback( + sock: c_int, + from: ?*const c.sockaddr, + addrlen: usize, + entry: c.mdns_entry_type_t, + query_id: u16, + rtype: u16, + rclass: u16, + ttl: u32, + data_ptr_opaque: ?*const anyopaque, + data_len: usize, + name_offset: usize, + name_length: usize, + record_offset: usize, + record_length: usize, + userdata: ?*anyopaque, + ) callconv(.C) c_int { + const log = std.log.scoped(.mdns); + if (entry != c.MDNS_ENTRYTYPE_QUESTION) return 0; + + const DNS_SD = "_services._dns-sd._udp.local."; + const this: *@This() = @ptrCast(@alignCast(userdata)); + _ = ttl; + _ = record_offset; + _ = record_length; + _ = name_length; + + // const data_ptr: [*]const u8 = @ptrCast(data_ptr_opaque.?); + // const data = data_ptr[0..data_len]; + + var name_buffer: [128]u8 = undefined; + var offset = name_offset; + const name_mdns_str = c.mdns_string_extract(data_ptr_opaque, data_len, &offset, &name_buffer, name_buffer.len); + const name = name_mdns_str.str[0..name_mdns_str.length]; + + 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)) { + const answer = c.mdns_record_t{ + .name = .{ .str = name.ptr, .length = name.len }, + .type = c.MDNS_RECORDTYPE_PTR, + .data = .{ .ptr = .{ .name = .{ .str = DNS_SD, .length = DNS_SD.len } } }, + }; + + var sendbuffer: [1024]u8 = undefined; + if (rclass & c.MDNS_UNICAST_RESPONSE != 0) { + const ret_val = c.mdns_query_answer_unicast( + sock, + from, + addrlen, + &sendbuffer, + sendbuffer.len, + query_id, + rtype, + name.ptr, + name.len, + answer, + 0, + 0, + 0, + 0, + ); + if (ret_val < 0) { + log.warn("{s}:{} failed to answer query, error code = {}", .{ @src().file, @src().line, ret_val }); + } + } else { + const ret_val = c.mdns_query_answer_multicast( + sock, + &sendbuffer, + sendbuffer.len, + answer, + 0, + 0, + 0, + 0, + ); + if (ret_val < 0) { + 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)) { + const answer = this.mdns_records[0]; + const additional = this.mdns_records[1..]; + + log.debug("heard service name \"{}\", sending answer {} and {} additional records", .{ std.zig.fmtEscapes(name), answer, additional.len }); + + var sendbuffer: [1024]u8 = undefined; + if (rclass & c.MDNS_UNICAST_RESPONSE != 0) { + log.debug("{s}:{} unicast response to {?}", .{ @src().file, @src().line, std_from }); + const ret_val = c.mdns_query_answer_unicast( + sock, + from, + addrlen, + &sendbuffer, + sendbuffer.len, + query_id, + rtype, + name.ptr, + name.len, + answer, + 0, + 0, + additional.ptr, + additional.len, + ); + if (ret_val < 0) { + log.warn("{s}:{} failed to answer query with unicast, error code = {}", .{ @src().file, @src().line, ret_val }); + } + } else { + log.debug("{s}:{} multicast response", .{ @src().file, @src().line }); + const ret_val = c.mdns_query_answer_multicast( + sock, + &sendbuffer, + sendbuffer.len, + answer, + 0, + 0, + additional.ptr, + additional.len, + ); + if (ret_val < 0) { + 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)) { + const answer = this.mdns_records[1]; + + var sendbuffer: [1024]u8 = undefined; + if (rclass & c.MDNS_UNICAST_RESPONSE != 0) { + _ = c.mdns_query_answer_unicast( + sock, + from, + addrlen, + &sendbuffer, + sendbuffer.len, + query_id, + rtype, + name.ptr, + name.len, + answer, + null, + 0, + null, + 0, + ); + } else { + _ = c.mdns_query_answer_multicast( + sock, + &sendbuffer, + sendbuffer.len, + answer, + null, + 0, + null, + 0, + ); + } + } + return 0; + } }; -fn host_multiplayer_game_handler(_: ?*anyopaque, request: Request) Handler.Error!Response { - var arena = std.heap.ArenaAllocator.init(request.allocator); - errdefer arena.deinit(); - - var text = try Element.Text.create(arena.allocator(), "Hosting Multiplayer Game"); - - var page = try Element.Page.create(arena.allocator()); - try page.addElement(.{ 0.5, 0.5 }, .{ 0.5, 0.5 }, text.element()); - - return Response{ .arena = arena, .body = .{ .element = page.element() } }; -} - const Handler = protocol.Handler; const Request = protocol.Request; const Response = protocol.Response; +const c = @import("./c.zig"); const protocol = @import("./protocol.zig"); const Element = @import("./Element.zig");