Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding classless IN-ADDR.ARPA (IPv4) and IP6.ARPA (IPv6) delegations (reverse lookups) #12

Closed
wants to merge 13 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ help me decide if this api is good: https://github.com/lun-4/zigdig/issues/10
- supports a subset of rdata (i do not have any plans to support 100% of DNS, but SRV/MX/TXT/A/AAAA
are there, which most likely will be enough for your use cases)
- has helpers for reading `/etc/resolv.conf` (not that much, really)
- classless IN-ADDR.ARPA (IPv4) and IP6.ARPA (IPv6) delegations (reverse lookups)

## what does it not do
- no edns0
Expand Down Expand Up @@ -55,7 +56,7 @@ pub fn main() !void {
defer {
_ = gpa.deinit();
}
var allocator = gpa.alloator();
var allocator = gpa.allocator();

var addresses = try dns.helpers.getAddressList("ziglang.org", allocator);
defer addresses.deinit();
Expand Down Expand Up @@ -144,6 +145,21 @@ pub fn main() !void {

```

### Reverse lookups examples
```
// Ipv4
const name = "dns.google.";
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

name is not being used in this example, why is that?

const test_address = "8.8.4.4";
var reverse = try dns.ReverseLookup.init(std.heap.page_allocator, test_address, 123);
const names = try reverse.lookupIpv4();

// Ipv6
const test_address = "2001:4860:4860::8888";
const name = "dns.google.";
var reverse = try dns.ReverseLookup.init(std.heap.page_allocator, test_address, 123);
const names = try reverse.lookupIpv6();
```

it is recommended to look at zigdig's source on `src/main.zig` to understand
how things tick using the library, but it boils down to three things:
- packet generation and serialization
Expand Down
160 changes: 160 additions & 0 deletions src/address.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
const std = @import("std");
const assert = std.debug.assert;

const AddressType = union(enum) { Ipv4, Ipv6 };

const Errors = error{InvalidIP};

pub const AddressMeta = union(enum) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why isn't std.net.Address used directly?

address: []const u8,
hexAddress: []const u8,
type: AddressType,

const Self = @This();

pub fn Ipv4() Self {
return Self{
.type = .Ipv4,
};
}

pub fn Ipv6() Self {
return Self{
.type = .Ipv6,
};
}

/// Creates an IpAddress from a []const u8 address
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AddressMeta.fromString doesn't return IpAddress

pub fn fromString(self: Self, ip_address: []const u8) !Self {
try self.valid(ip_address);

return AddressMeta{ .address = ip_address };
}

/// Validates the calling ip address is actually valid
pub fn valid(self: Self, ip_address: []const u8) !void {
switch (self.type) {
.Ipv4 => {
_ = try std.net.Ip4Address.parse(ip_address, 0);
},
.Ipv6 => {
_ = try std.net.Ip6Address.parse(ip_address, 0);
},
}
}
};

// RFC reference for ARPA address formation: https://www.ietf.org/rfc/rfc2317.txt
pub const IpAddress = struct {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this probably should be renamed to ArpaDomain rather than IpAddress

address: AddressMeta,
leastSignificationShiftValue: u16 = 0xFF, // Least significant bitmask value
// Masks for nibble shifting for ipv6
nib_shift_low: u16 = 0x0F, // Lowest significant bit
nib_shift_high: u16 = 0xF0, // Most significant bit
arpa_suffix: []const u8 = ".in-addr.arpa",
arpa_suffix_ipv6: []const u8 = "ip6.arpa",

allocator: std.mem.Allocator,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the design of zigdig allows for library users to interact with it without memory allocation, is there a way to do such here?


const Self = @This();

pub fn init(allocator: std.mem.Allocator, address: AddressMeta) !Self {
return Self{
.allocator = allocator,
.address = address,
};
}

/// Reverse IP address for Classless IN-ADDR.ARPA delegation
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add Caller owns returned memory as is convention for zig standard library

this also applies to reverseIpv6

pub fn reverseIpv4(self: Self) ![]const u8 {
const ip = try std.net.Ip4Address.parse(self.address.address, 0);
// ip.sa.addr is the raw addr u32 representation of the parsed address.
var shifted_ip = self.bitmask(ip.sa.addr);

// Just use native zig reverse
std.mem.reverse(u32, &shifted_ip);

// Note - If we buf print here, buffer will fill with nullbytes when we use dns.Name.fromString(buf, &alloc_locatio); So we alloc print here to avoid future complications
return try std.fmt.allocPrint(self.allocator, "{d}.{d}.{d}.{d}{s}", .{ shifted_ip[0], shifted_ip[1], shifted_ip[2], shifted_ip[3], self.arpa_suffix });
}

/// Reverse ipv6 address as per RFC: https://datatracker.ietf.org/doc/html/rfc3596
/// https://datatracker.ietf.org/doc/html/rfc3596#section-2.5 (notable section referenced)
pub fn reverseIpv6(self: Self) ![]const u8 {
var parsed_address = try std.net.Ip6Address.parse(self.address.address, 0);

std.mem.reverse(u8, &parsed_address.sa.addr);

var ipv6_parsed: []const u8 = undefined;
var index: usize = 0;
for (parsed_address.sa.addr) |v| {
// v is a byte at this point (8 bits)
// Create a nibble, bitshift/swap the nibble values and convert to hex to build the arpa address
const low_nibble = v & self.nib_shift_low;
const high_nibble = ((v & self.nib_shift_high) >> 4);

// This string formatting is a little annoying and there may be a better way
if (index == 0) {
ipv6_parsed = try std.fmt.allocPrint(std.heap.page_allocator, "{x}.{x}", .{ low_nibble, high_nibble });
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the given self.allocator should be used instead of std.heap.page_allocator for efficiency's sake.


index += 1;

continue;
}

if (index == parsed_address.sa.addr.len - 1) {
ipv6_parsed = try std.fmt.allocPrint(std.heap.page_allocator, "{s}.{x}.{x}.{s}", .{ ipv6_parsed, low_nibble, high_nibble, self.arpa_suffix_ipv6 });
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this function leaks memory as the previous allocPrint invocation's memory isn't freed. using the GPA / std.heap.GeneralPurposeAllocator should have shown this (this relates to not using self.allocator, as page_allocator does not do leak checking nor double-free checking)

} else {
ipv6_parsed = try std.fmt.allocPrint(std.heap.page_allocator, "{s}.{x}.{x}", .{ ipv6_parsed, low_nibble, high_nibble });
}

index += 1;
}

return ipv6_parsed;
}

/// Converts from the little-endian hex values. Used for addresses stored on disk (Unix hosts) from sectors like /proc/net/tcp || /proc/net/udp
pub fn hexConvertAddress(self: Self) ![4]u32 {
return self.bitmask(try std.fmt.parseInt(u32, self.address.hexAddress, 16));
}

// Bit masking to ascertain least significant bit for parsing Ipv4 out of u32
fn bitmask(self: Self, value: u32) [4]u32 {
const b1 = (value & self.leastSignificationShiftValue);
const b2 = (value >> 8) & self.leastSignificationShiftValue;
const b3 = (value >> 16) & self.leastSignificationShiftValue;
const b4 = (value >> 24) & self.leastSignificationShiftValue;

return [4]u32{ b1, b2, b3, b4 };
}
};

test "test bitmask and reverseral of ip address" {
const ip = IpAddress{ .address = .{ .address = "8.8.4.4" }, .allocator = std.heap.page_allocator };
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use std.testing.allocator as that is a shim over the general purpose allocator

(applies to other tests)

const reversed = try ip.reverseIpv4();

assert(std.mem.eql(u8, reversed, "4.4.8.8.in-addr.arpa"));
}

test "text hex conversion into IP address" {
const hex_address: []const u8 = "0100007F"; // Converted to "0x0100007F for 127.0.0.1"

const ip = IpAddress{ .address = .{ .hexAddress = hex_address }, .allocator = std.heap.page_allocator };
const hex_converted = try ip.hexConvertAddress();

var buf: [512]u8 = undefined;
const c_val = try std.fmt.bufPrint(&buf, "{d}.{d}.{d}.{d}", .{ hex_converted[0], hex_converted[1], hex_converted[2], hex_converted[3] });

assert(std.mem.eql(u8, c_val, "127.0.0.1"));
}

test "test reversal of ipv6 address" {
const address_reversed = "b.a.9.8.7.6.5.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa";
const address = "2001:db8::567:89ab";

const ip = IpAddress{ .address = .{ .address = address }, .allocator = std.heap.page_allocator };
const ipv6_parsed = try ip.reverseIpv6();

assert(std.mem.eql(u8, address_reversed, ipv6_parsed));
}
7 changes: 7 additions & 0 deletions src/lib.zig
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ pub const Name = names.Name;
pub const LabelComponent = names.LabelComponent;
pub const NamePool = names.NamePool;

pub const address = @import("address.zig");
pub const IpAddress = address.IpAddress;
pub const IpAdressMeta = address.AddressMeta;

pub const reverse = @import("reverse.zig");
pub const ReverseLookup = reverse.ReverseLookup;

const pkt = @import("packet.zig");
pub const Packet = pkt.Packet;
pub const ResponseCode = pkt.ResponseCode;
Expand Down
4 changes: 2 additions & 2 deletions src/name.zig
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,8 @@ pub const Name = union(enum) {

// set first two bits of ptr_offset to zero as they're the
// pointer prefix bits (which are always 1, which brings problems)
offset &= ~@as(u16, 1 << 15);
offset &= ~@as(u16, 1 << 14);
// Do this with a bitmask operation
offset &= 0x3FFF;

return LabelComponent{ .Pointer = offset };
} else {
Expand Down
2 changes: 1 addition & 1 deletion src/parser.zig
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ pub const ParserOptions = struct {
///
/// Makes parser return `error.Overflow` when
/// the given name to deserialize surpasses the value in this field.
max_label_size: usize = 32,
max_label_size: usize = 35, // Note: max label size *must* match the max label size (max_ipv6_label_size) within reverse.zig Reverse struct
};

pub const ParserContext = struct {
Expand Down
151 changes: 151 additions & 0 deletions src/reverse.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
const std = @import("std");
const address = @import("./address.zig");
const dns = @import("./lib.zig");
const assert = std.debug.assert;

/// ReverseLookup is the primary interface for classless IN-ADDR.ARPA (IPv4) and IP6.ARPA (IPv6) delegations
pub const ReverseLookup = struct {
allocator: std.mem.Allocator,
ip_address: []const u8,
packet_id: u16, // Arbitrary packet ID

const Self = @This();
const max_ipv6_label_size = 35;
const max_ipv4_label_size = 6;

pub fn init(allocator: std.mem.Allocator, ip_address: []const u8, packet_id: u16) !Self {
return Self{
.allocator = allocator,
.ip_address = ip_address,
.packet_id = packet_id,
};
}

/// Reverse lookup on a given ipv4 ip address
pub fn lookupIpv4(self: Self) ![][]const u8 {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if std.net.Address was used, I think you can create a much more direct lookup() function instead of having to redo it via AddressMeta

var add = try address.IpAddress.init(self.allocator, try address.AddressMeta.Ipv4().fromString(self.ip_address));
const arpa_address = try add.reverseIpv4();

var labels: [max_ipv4_label_size][]const u8 = undefined;
const apra_address_dns_name = try dns.Name.fromString(arpa_address, &labels);

return try self.buildAndSendPacket(apra_address_dns_name);
}

/// Reverse lookup on a given ipv6 ip address
pub fn lookupIpv6(self: Self) ![][]const u8 {
var add = try address.IpAddress.init(self.allocator, try address.AddressMeta.Ipv6().fromString(self.ip_address));
const arpa_address = try add.reverseIpv6();

var labels: [max_ipv6_label_size][]const u8 = undefined;
const apra_address_dns_name = try dns.Name.fromString(arpa_address, &labels);

return self.buildAndSendPacket(apra_address_dns_name) catch &[_][]const u8{};
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should return the error instead of an empty stack-allocated array. currently this makes it not clear who owns the memory given by this function, as an error in buildAndSendPacket() will cause a library user to attempt to call allocator.free() (given they had to give an allocator to this function) a memory region that is on the stack, causing segmentation fault on non-zero arrays (which this could be, given [_]).

example:

const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();
    const coolData = &[1][]const u8{"test"};
    allocator.free(coolData);
}

Segmentation fault at address 0x1003ef0
/home/luna/.zvm/0.13.0/lib/compiler_rt/memset.zig:19:14: 0x10e6f50 in memset (compiler_rt)
            d[0] = c;
             ^
/home/luna/.zvm/0.13.0/lib/std/mem/Allocator.zig:313:26: 0x103780f in free__anon_3488 (test65)
    @memset(non_const_ptr[0..bytes_len], undefined);
                         ^
/home/luna/test-zig/test65.zig:8:19: 0x10376d6 in main (test65)
    allocator.free(coolData);
                  ^
/home/luna/.zvm/0.13.0/lib/std/start.zig:524:37: 0x1037654 in posixCallMainAndExit (test65)
            const result = root.main() catch |err| {
                                    ^
/home/luna/.zvm/0.13.0/lib/std/start.zig:266:5: 0x1037191 in _start (test65)
    asm volatile (switch (native_arch) {
    ^
???:?:?: 0x0 in ??? (???)
fish: Job 1, 'zig run test65.zig' terminated by signal SIGABRT (Abort)

}

/// Internal function to build and send the DNS packet for reverse lookup agnostic of IP address type (ipv4 | ipv6)
fn buildAndSendPacket(self: Self, apra_address_dns_name: dns.Name) ![][]const u8 {
var name_pool = dns.NamePool.init(self.allocator);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if this helper class can not operate with an allocator-free paradigm, then it should be marked as such in the main doc comment for ReverseLookup

defer name_pool.deinitWithNames();

var question = [_]dns.Question{.{
.class = .IN,
.typ = .PTR,
.name = apra_address_dns_name,
}};

var empty = [_]dns.Resource{};

const packet = dns.Packet{
.header = .{
.id = self.packet_id,
.wanted_recursion = true, // Need recursion of at least 1 depth for reverse lookup
.answer_length = 0, // 0 for reverse query
.question_length = 1,
.nameserver_length = 0,
.additional_length = 0,
.opcode = .Query,
},
.questions = &question,
.answers = &empty,
.nameservers = &empty,
.additionals = &[_]dns.Resource{},
};

const conn = try dns.helpers.connectToSystemResolver();
defer conn.close();
try conn.sendPacket(packet);

const reply = try conn.receiveFullPacket(
self.allocator,
4096,
.{ .name_pool = &name_pool },
);
defer reply.deinit(.{ .names = false });

const reply_packet = reply.packet;

// Parse reply packet, if there are answers return them in a serialized fashion for human consumption. Else empty
if (reply_packet.answers.len > 0) {
var dns_names = try self.allocator.alloc([]u8, reply_packet.answers.len);
var index: usize = 0;
for (reply_packet.answers) |resource| {
const resource_data = try dns.ResourceData.fromOpaque(
resource.typ,
resource.opaque_rdata.?,
.{
.name_provider = .{ .full = &name_pool },
.allocator = name_pool.allocator,
},
);

//fromOpaque read the data, so if we utilize any standard reader/writer we have access to the []const u8 opaque data value. So we just allocprint here
const dns_name = try std.fmt.allocPrint(self.allocator, "{s}", .{resource_data});
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should verify that the returned resource_data is of type PTR instead of relying on the dynamic format within dns.ResourceData. a malformed response would return malformed results to library clients instead of an error or empty slice.


dns_names[index] = dns_name;
index += 1;
}

return dns_names;
} else {
return &[_][]const u8{}; // empty
}
}
};

test "reverse lookup of Ipv4 address" {
const name = "dns.google.";
const test_address = "8.8.4.4";
var reverse = try ReverseLookup.init(std.heap.page_allocator, test_address, 123);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use std.testing.allocator for memory leak checking (applies to all tests here as well)

const names = try reverse.lookupIpv4();

assert(names.len > 0);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use std.testing.expect to fail the test with an error instead of panic'ing the entire executable

assert(std.mem.eql(u8, names[0], name));
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use std.testing.expectEqualStrings for the same reason, plus gives nice diff output when it fails


// Test when no name matches
const non_address = "123.123.123.123";
reverse = try ReverseLookup.init(std.heap.page_allocator, non_address, 123);
const names_non = try reverse.lookupIpv4();

// This should be empty
assert(names_non.len == 0);
}

test "reverse lookup of ipv6" {
// Test existing Ipv6 address (google dns - same as 8.8.4.4)
const google_ipv6_address = "2001:4860:4860::8888";
const name = "dns.google.";
var reverse = try ReverseLookup.init(std.heap.page_allocator, google_ipv6_address, 123);
const names = try reverse.lookupIpv6();

assert(names.len > 0);
assert(std.mem.eql(u8, names[0], name));

// // Test when no name matches (localhost ipv6)
const non_existent_ipv6 = "2001:6665:1234::1234";
reverse = try ReverseLookup.init(std.heap.page_allocator, non_existent_ipv6, 124);
const names_non = try reverse.lookupIpv6();

// This should be empty
assert(names_non.len == 0);
}
Loading