diff --git a/capsule-core/src/discovery.zig b/capsule-core/src/discovery.zig index b52d3c0..419d4dc 100644 --- a/capsule-core/src/discovery.zig +++ b/capsule-core/src/discovery.zig @@ -154,34 +154,138 @@ pub const DiscoveryService = struct { } /// Parse an incoming mDNS packet and update the peer table + /// Enhanced parser: Extracts real node_id from PTR RDATA pub fn handlePacket(self: *DiscoveryService, peer_table: anytype, buf: []const u8, sender: net.Address) !void { - if (buf.len < 12) return; // Too small + if (buf.len < 12) return; // Too small for DNS header - // Skip Header (12 bytes) + // Parse DNS Header const answer_count = std.mem.readInt(u16, buf[6..8], .big); if (answer_count == 0) return; - // Skip Question section if any (simplified: we expect responses to our query or gratuitous responses) - // For local discovery, we mostly care about Answers. + // Skip Question section (after header) + const question_count = std.mem.readInt(u16, buf[4..6], .big); + var offset: usize = 12; - // This is a VERY MINIMAL parser for Week 28. - // It looks for the "_libertaria._udp.local" string and assumes the following PTR. - if (std.mem.indexOf(u8, buf, "_libertaria")) |_| { - // Found a Libertaria record! - // In a real implementation, we'd parse SRV/TXT for the actual port and DID. - // For MVP, if we receive a Libertaria-tagged packet, we trust the sender's IP. - // (Port is still tricky since discovery is on 5353 but service is on 8710). + // Skip Question section + var i: u16 = 0; + while (i < question_count) : (i += 1) { + // Skip NAME + while (offset < buf.len) { + const label_len = buf[offset]; + offset += 1; + if (label_len == 0) break; + if (label_len & 0xC0 == 0xC0) { + // Pointer, skip 2 bytes total + offset += 1; + break; + } + offset += label_len; + } + // Skip TYPE(2) + CLASS(2) + if (offset + 4 > buf.len) break; + offset += 4; + } - // TODO: Extract DID from TXT record - var mock_did = [_]u8{0} ** 8; - @memcpy(mock_did[0..4], "NODE"); + // Parse Answer section + i = 0; + while (i < answer_count) : (i += 1) { + if (offset + 10 > buf.len) break; // Need at least: NAME + TYPE(2) + CLASS(2) + TTL(4) + RDLEN(2) - // We assume the peer is running on its default port or we need SRV record. - // For now, use the sender's IP but the standard port. - var peer_addr = sender; - peer_addr.setPort(self.port); // Fallback to our configured port if unknown + // Parse NAME (could be pointer or full name) + var found_libertaria = false; + while (offset < buf.len) { + const label_len = buf[offset]; + if (label_len == 0) { + offset += 1; + break; + } + if (label_len & 0xC0 == 0xC0) { + // Pointer, skip 2 bytes total + offset += 2; + break; + } + offset += 1; + // Check for "_libertaria" in label + if (label_len >= 11 and offset + label_len <= buf.len) { + const label = buf[offset..][0..label_len]; + if (std.mem.indexOf(u8, label, "_libertaria")) |_| { + found_libertaria = true; + } + } + offset += label_len; + } - try peer_table.updatePeer(mock_did, peer_addr); + // Read TYPE (2) + CLASS (2) + TTL (4) + RDLEN (2) + if (offset + 10 > buf.len) break; + const type_val = std.mem.readInt(u16, buf[offset..][0..2], .big); + offset += 2; // Skip TYPE + offset += 2; // Skip CLASS + offset += 4; // Skip TTL + const rdlen = std.mem.readInt(u16, buf[offset..][0..2], .big); + offset += 2; + + // Check if this is a PTR record with Libertaria + if (found_libertaria and type_val == 12) { // Type 12 = PTR + const rdata_start = offset; + const rdata_end = offset + @as(usize, rdlen); + if (rdata_end > buf.len) break; + + // Extract node_id from PTR RDATA (format: dde9b2d5c247._libertaria._udp.local) + var extracted_hex = [_]u8{0} ** 16; + var hex_len: usize = 0; + + var name_offset = rdata_start; + while (name_offset < rdata_end and hex_len < 16) { + const label_len = buf[name_offset]; + if (label_len == 0 or label_len & 0xC0 == 0xC0) break; + if (label_len > 16) break; // Too long for hex + + // Check if this is the hex-encoded node_id + const label = buf[name_offset + 1 ..][0..label_len]; + var is_hex = true; + for (label) |c| { + if (!((c >= '0' and c <= '9') or (c >= 'a' and c <= 'f'))) { + is_hex = false; + break; + } + } + + if (is_hex) { + @memcpy(extracted_hex[hex_len..][0..label.len], label); + hex_len += label.len; + } + + name_offset += 1 + label_len; + } + + // Parse hex string back to bytes + var peer_id = [_]u8{0} ** 8; // 8-byte peer ID (matches announce format) + var byte_pos: usize = 0; + for (0..@min(hex_len / 2, 8)) |j| { + if (j * 2 + 1 < hex_len) { + const high = try std.fmt.charToDigit(extracted_hex[j * 2], 16); + const low = try std.fmt.charToDigit(extracted_hex[j * 2 + 1], 16); + if (byte_pos < peer_id.len) { + peer_id[byte_pos] = @as(u8, @truncate((high << 4) | low)); + byte_pos += 1; + } + } + } + + // Skip our own node_id (compare first 8 bytes) + if (!std.mem.eql(u8, &peer_id, self.node_id[0..8])) { + // We assume the peer is running on its default port or we need SRV record. + // For now, use the sender's IP but the standard port. + var peer_addr = sender; + peer_addr.setPort(self.port); // Fallback to our configured port if unknown + + std.log.info("Discovery: Added peer {x} from port {d}", .{peer_id[0..byte_pos], sender.getPort()}); + try peer_table.updatePeer(peer_id, peer_addr); + } + } + + // Skip RDATA + offset += @as(usize, rdlen); } } };