mirror of
https://github.com/odin-lang/Odin.git
synced 2025-12-29 09:24:33 +00:00
858 lines
21 KiB
Odin
858 lines
21 KiB
Odin
// +build windows, linux, darwin
|
|
package net
|
|
|
|
/*
|
|
Package net implements cross-platform Berkeley Sockets, DNS resolution and associated procedures.
|
|
For other protocols and their features, see subdirectories of this package.
|
|
*/
|
|
|
|
/*
|
|
Copyright 2022 Tetralux <tetraluxonpc@gmail.com>
|
|
Copyright 2022 Colin Davidson <colrdavidson@gmail.com>
|
|
Copyright 2022 Jeroen van Rijn <nom@duclavier.com>.
|
|
Made available under Odin's BSD-3 license.
|
|
|
|
List of contributors:
|
|
Tetralux: Initial implementation
|
|
Colin Davidson: Linux platform code, OSX platform code, Odin-native DNS resolver
|
|
Jeroen van Rijn: Cross platform unification, code style, documentation
|
|
*/
|
|
|
|
import "core:mem"
|
|
import "core:strings"
|
|
import "core:time"
|
|
import "core:os"
|
|
/*
|
|
Default configuration for DNS resolution.
|
|
*/
|
|
when ODIN_OS == .Windows {
|
|
DEFAULT_DNS_CONFIGURATION :: DNS_Configuration{
|
|
resolv_conf = "",
|
|
hosts_file = "%WINDIR%\\system32\\drivers\\etc\\hosts",
|
|
}
|
|
} else when ODIN_OS == .Linux || ODIN_OS == .Darwin || ODIN_OS == .OpenBSD {
|
|
DEFAULT_DNS_CONFIGURATION :: DNS_Configuration{
|
|
resolv_conf = "/etc/resolv.conf",
|
|
hosts_file = "/etc/hosts",
|
|
}
|
|
} else {
|
|
#panic("Please add a configuration for this OS.")
|
|
}
|
|
|
|
@(init)
|
|
init_dns_configuration :: proc() {
|
|
/*
|
|
Resolve %ENVIRONMENT% placeholders in their paths.
|
|
*/
|
|
dns_configuration.resolv_conf, _ = replace_environment_path(dns_configuration.resolv_conf)
|
|
dns_configuration.hosts_file, _ = replace_environment_path(dns_configuration.hosts_file)
|
|
}
|
|
|
|
destroy_dns_configuration :: proc() {
|
|
delete(dns_configuration.resolv_conf)
|
|
delete(dns_configuration.hosts_file)
|
|
}
|
|
|
|
dns_configuration := DEFAULT_DNS_CONFIGURATION
|
|
|
|
// Always allocates for consistency.
|
|
replace_environment_path :: proc(path: string, allocator := context.allocator) -> (res: string, ok: bool) {
|
|
// Nothing to replace. Return a clone of the original.
|
|
if strings.count(path, "%") != 2 {
|
|
return strings.clone(path, allocator), true
|
|
}
|
|
|
|
left := strings.index(path, "%") + 1
|
|
assert(left > 0 && left <= len(path)) // should be covered by there being two %
|
|
|
|
right := strings.index(path[left:], "%") + 1
|
|
assert(right > 0 && right <= len(path)) // should be covered by there being two %
|
|
|
|
env_key := path[left: right]
|
|
env_val := os.get_env(env_key, allocator)
|
|
defer delete(env_val)
|
|
|
|
res, _ = strings.replace(path, path[left - 1: right + 1], env_val, 1, allocator)
|
|
return res, true
|
|
}
|
|
|
|
|
|
/*
|
|
Resolves a hostname to exactly one IP4 and IP6 endpoint.
|
|
It's then up to you which one you use.
|
|
Note that which address you use to open a socket, determines the type of the socket you get.
|
|
|
|
Returns `ok=false` if the host name could not be resolved to any endpoints.
|
|
|
|
Returned endpoints have the same port as provided in the string, or 0 if absent.
|
|
If you want to use a specific port, just modify the field after the call to this procedure.
|
|
|
|
If the hostname part of the endpoint is actually a string representation of an IP address, DNS resolution will be skipped.
|
|
This allows you to pass both strings like "example.com:9000" and "1.2.3.4:9000" to this function end reliably get
|
|
back an endpoint in both cases.
|
|
*/
|
|
resolve :: proc(hostname_and_maybe_port: string) -> (ep4, ep6: Endpoint, err: Network_Error) {
|
|
target := parse_hostname_or_endpoint(hostname_and_maybe_port) or_return
|
|
switch t in target {
|
|
case Endpoint:
|
|
// NOTE(tetra): The hostname was actually an IP address; nothing to resolve, so just return it.
|
|
switch _ in t.address {
|
|
case IP4_Address: ep4 = t
|
|
case IP6_Address: ep6 = t
|
|
case: unreachable()
|
|
}
|
|
return
|
|
|
|
case Host:
|
|
err4, err6: Network_Error = ---, ---
|
|
ep4, err4 = resolve_ip4(t.hostname)
|
|
ep6, err6 = resolve_ip6(t.hostname)
|
|
ep4.port = t.port if err4 == nil else 0
|
|
ep6.port = t.port if err6 == nil else 0
|
|
if err4 != nil && err6 != nil {
|
|
err = err4
|
|
}
|
|
return
|
|
}
|
|
unreachable()
|
|
}
|
|
|
|
resolve_ip4 :: proc(hostname_and_maybe_port: string) -> (ep4: Endpoint, err: Network_Error) {
|
|
target := parse_hostname_or_endpoint(hostname_and_maybe_port) or_return
|
|
switch t in target {
|
|
case Endpoint:
|
|
// NOTE(tetra): The hostname was actually an IP address; nothing to resolve, so just return it.
|
|
switch _ in t.address {
|
|
case IP4_Address:
|
|
return t, nil
|
|
case IP6_Address:
|
|
err = .Unable_To_Resolve
|
|
return
|
|
}
|
|
case Host:
|
|
recs, _ := get_dns_records_from_os(t.hostname, .IP4, context.temp_allocator)
|
|
if len(recs) == 0 {
|
|
err = .Unable_To_Resolve
|
|
return
|
|
}
|
|
ep4 = {
|
|
address = recs[0].(DNS_Record_IP4).address,
|
|
port = t.port,
|
|
}
|
|
return
|
|
}
|
|
unreachable()
|
|
}
|
|
|
|
resolve_ip6 :: proc(hostname_and_maybe_port: string) -> (ep6: Endpoint, err: Network_Error) {
|
|
target := parse_hostname_or_endpoint(hostname_and_maybe_port) or_return
|
|
switch t in target {
|
|
case Endpoint:
|
|
// NOTE(tetra): The hostname was actually an IP address; nothing to resolve, so just return it.
|
|
switch _ in t.address {
|
|
case IP4_Address:
|
|
err = .Unable_To_Resolve
|
|
return
|
|
case IP6_Address:
|
|
return t, nil
|
|
}
|
|
case Host:
|
|
recs, _ := get_dns_records_from_os(t.hostname, .IP6, context.temp_allocator)
|
|
if len(recs) == 0 {
|
|
err = .Unable_To_Resolve
|
|
return
|
|
}
|
|
ep6 = {
|
|
address = recs[0].(DNS_Record_IP6).address,
|
|
port = t.port,
|
|
}
|
|
return
|
|
}
|
|
unreachable()
|
|
}
|
|
|
|
/*
|
|
Performs a recursive DNS query for records of a particular type for the hostname using the OS.
|
|
|
|
NOTE: This procedure instructs the DNS resolver to recursively perform CNAME requests on our behalf,
|
|
meaning that DNS queries for a hostname will resolve through CNAME records until an
|
|
IP address is reached.
|
|
|
|
IMPORTANT: This procedure allocates memory for each record returned; deleting just the returned slice is not enough!
|
|
See `destroy_records`.
|
|
*/
|
|
get_dns_records_from_os :: proc(hostname: string, type: DNS_Record_Type, allocator := context.allocator) -> (records: []DNS_Record, err: DNS_Error) {
|
|
return _get_dns_records_os(hostname, type, allocator)
|
|
}
|
|
|
|
/*
|
|
A generic DNS client usable on any platform.
|
|
Performs a recursive DNS query for records of a particular type for the hostname.
|
|
|
|
NOTE: This procedure instructs the DNS resolver to recursively perform CNAME requests on our behalf,
|
|
meaning that DNS queries for a hostname will resolve through CNAME records until an
|
|
IP address is reached.
|
|
|
|
IMPORTANT: This procedure allocates memory for each record returned; deleting just the returned slice is not enough!
|
|
See `destroy_records`.
|
|
*/
|
|
get_dns_records_from_nameservers :: proc(hostname: string, type: DNS_Record_Type, name_servers: []Endpoint, host_overrides: []DNS_Record, allocator := context.allocator) -> (records: []DNS_Record, err: DNS_Error) {
|
|
context.allocator = allocator
|
|
|
|
if type != .SRV {
|
|
// NOTE(tetra): 'hostname' can contain underscores when querying SRV records
|
|
ok := validate_hostname(hostname)
|
|
if !ok {
|
|
return nil, .Invalid_Hostname_Error
|
|
}
|
|
}
|
|
|
|
hdr := DNS_Header{
|
|
id = 0,
|
|
is_response = false,
|
|
opcode = 0,
|
|
is_authoritative = false,
|
|
is_truncated = false,
|
|
is_recursion_desired = true,
|
|
is_recursion_available = false,
|
|
response_code = DNS_Response_Code.No_Error,
|
|
}
|
|
|
|
id, bits := pack_dns_header(hdr)
|
|
dns_hdr := [6]u16be{}
|
|
dns_hdr[0] = id
|
|
dns_hdr[1] = bits
|
|
dns_hdr[2] = 1
|
|
|
|
dns_query := [2]u16be{ u16be(type), 1 }
|
|
|
|
output := [(size_of(u16be) * 6) + NAME_MAX + (size_of(u16be) * 2)]u8{}
|
|
b := strings.builder_from_slice(output[:])
|
|
|
|
strings.write_bytes(&b, mem.slice_data_cast([]u8, dns_hdr[:]))
|
|
ok := encode_hostname(&b, hostname)
|
|
if !ok {
|
|
return nil, .Invalid_Hostname_Error
|
|
}
|
|
strings.write_bytes(&b, mem.slice_data_cast([]u8, dns_query[:]))
|
|
|
|
dns_packet := output[:strings.builder_len(b)]
|
|
|
|
dns_response_buf := [4096]u8{}
|
|
dns_response: []u8
|
|
for name_server in name_servers {
|
|
conn, sock_err := make_unbound_udp_socket(family_from_endpoint(name_server))
|
|
if sock_err != nil {
|
|
return nil, .Connection_Error
|
|
}
|
|
defer close(conn)
|
|
|
|
_ = send(conn, dns_packet[:], name_server) or_continue
|
|
|
|
if set_option(conn, .Receive_Timeout, time.Second * 1) != nil {
|
|
return nil, .Connection_Error
|
|
}
|
|
|
|
// recv_sz, _, recv_err := recv_udp(conn, dns_response_buf[:])
|
|
// if recv_err == UDP_Recv_Error.Timeout {
|
|
// continue
|
|
// } else if recv_err != nil {
|
|
// continue
|
|
// }
|
|
recv_sz, _ := recv_udp(conn, dns_response_buf[:]) or_continue
|
|
if recv_sz == 0 {
|
|
continue
|
|
}
|
|
|
|
dns_response = dns_response_buf[:recv_sz]
|
|
|
|
rsp, _ok := parse_response(dns_response, type)
|
|
if !_ok {
|
|
return nil, .Server_Error
|
|
}
|
|
|
|
if len(rsp) == 0 {
|
|
continue
|
|
}
|
|
|
|
return rsp[:], nil
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// `records` slice is also destroyed.
|
|
destroy_dns_records :: proc(records: []DNS_Record, allocator := context.allocator) {
|
|
context.allocator = allocator
|
|
|
|
for rec in records {
|
|
switch r in rec {
|
|
case DNS_Record_IP4:
|
|
delete(r.base.record_name)
|
|
|
|
case DNS_Record_IP6:
|
|
delete(r.base.record_name)
|
|
|
|
case DNS_Record_CNAME:
|
|
delete(r.base.record_name)
|
|
delete(r.host_name)
|
|
|
|
case DNS_Record_TXT:
|
|
delete(r.base.record_name)
|
|
delete(r.value)
|
|
|
|
case DNS_Record_NS:
|
|
delete(r.base.record_name)
|
|
delete(r.host_name)
|
|
|
|
case DNS_Record_MX:
|
|
delete(r.base.record_name)
|
|
delete(r.host_name)
|
|
|
|
case DNS_Record_SRV:
|
|
delete(r.record_name)
|
|
delete(r.target)
|
|
}
|
|
}
|
|
|
|
delete(records, allocator)
|
|
}
|
|
|
|
/*
|
|
TODO(cloin): Does the DNS Resolver need to recursively hop through CNAMEs to get the IP
|
|
or is that what recursion desired does? Do we need to handle recursion unavailable?
|
|
How do we deal with is_authoritative / is_truncated?
|
|
*/
|
|
|
|
NAME_MAX :: 255
|
|
LABEL_MAX :: 63
|
|
|
|
pack_dns_header :: proc(hdr: DNS_Header) -> (id: u16be, bits: u16be) {
|
|
id = hdr.id
|
|
bits = hdr.opcode << 1 | u16be(hdr.response_code)
|
|
if hdr.is_response {
|
|
bits |= 1 << 15
|
|
}
|
|
if hdr.is_authoritative {
|
|
bits |= 1 << 10
|
|
}
|
|
if hdr.is_truncated {
|
|
bits |= 1 << 9
|
|
}
|
|
if hdr.is_recursion_desired {
|
|
bits |= 1 << 8
|
|
}
|
|
if hdr.is_recursion_available {
|
|
bits |= 1 << 7
|
|
}
|
|
|
|
return id, bits
|
|
}
|
|
|
|
unpack_dns_header :: proc(id: u16be, bits: u16be) -> (hdr: DNS_Header) {
|
|
hdr.id = id
|
|
hdr.is_response = (bits & (1 << 15)) != 0
|
|
hdr.opcode = (bits >> 11) & 0xF
|
|
hdr.is_authoritative = (bits & (1 << 10)) != 0
|
|
hdr.is_truncated = (bits & (1 << 9)) != 0
|
|
hdr.is_recursion_desired = (bits & (1 << 8)) != 0
|
|
hdr.is_recursion_available = (bits & (1 << 7)) != 0
|
|
hdr.response_code = DNS_Response_Code(bits & 0xF)
|
|
|
|
return hdr
|
|
}
|
|
|
|
load_resolv_conf :: proc(resolv_conf_path: string, allocator := context.allocator) -> (name_servers: []Endpoint, ok: bool) {
|
|
context.allocator = allocator
|
|
|
|
res := os.read_entire_file_from_filename(resolv_conf_path) or_return
|
|
defer delete(res)
|
|
resolv_str := string(res)
|
|
|
|
id_str := "nameserver"
|
|
id_len := len(id_str)
|
|
|
|
_name_servers := make([dynamic]Endpoint, 0, allocator)
|
|
for line in strings.split_lines_iterator(&resolv_str) {
|
|
if len(line) == 0 || line[0] == '#' {
|
|
continue
|
|
}
|
|
|
|
if len(line) < id_len || strings.compare(line[:id_len], id_str) != 0 {
|
|
continue
|
|
}
|
|
|
|
server_ip_str := strings.trim_left_space(line[id_len:])
|
|
if len(server_ip_str) == 0 {
|
|
continue
|
|
}
|
|
|
|
addr := parse_address(server_ip_str)
|
|
if addr == nil {
|
|
continue
|
|
}
|
|
|
|
endpoint := Endpoint{
|
|
addr,
|
|
53,
|
|
}
|
|
append(&_name_servers, endpoint)
|
|
}
|
|
|
|
return _name_servers[:], true
|
|
}
|
|
|
|
load_hosts :: proc(hosts_file_path: string, allocator := context.allocator) -> (hosts: []DNS_Host_Entry, ok: bool) {
|
|
context.allocator = allocator
|
|
|
|
res := os.read_entire_file_from_filename(hosts_file_path, allocator) or_return
|
|
defer delete(res)
|
|
|
|
_hosts := make([dynamic]DNS_Host_Entry, 0, allocator)
|
|
hosts_str := string(res)
|
|
for line in strings.split_lines_iterator(&hosts_str) {
|
|
if len(line) == 0 || line[0] == '#' {
|
|
continue
|
|
}
|
|
|
|
splits := strings.fields(line)
|
|
defer delete(splits)
|
|
|
|
ip_str := splits[0]
|
|
addr := parse_address(ip_str)
|
|
if addr == nil {
|
|
continue
|
|
}
|
|
|
|
for hostname in splits[1:] {
|
|
if len(hostname) != 0 {
|
|
append(&_hosts, DNS_Host_Entry{hostname, addr})
|
|
}
|
|
}
|
|
}
|
|
|
|
return _hosts[:], true
|
|
}
|
|
|
|
// www.google.com -> 3www6google3com0
|
|
encode_hostname :: proc(b: ^strings.Builder, hostname: string) -> (ok: bool) {
|
|
_hostname := hostname
|
|
for section in strings.split_iterator(&_hostname, ".") {
|
|
if len(section) > LABEL_MAX {
|
|
return
|
|
}
|
|
|
|
strings.write_byte(b, u8(len(section)))
|
|
strings.write_string(b, section)
|
|
}
|
|
strings.write_byte(b, 0)
|
|
|
|
return true
|
|
}
|
|
|
|
skip_hostname :: proc(packet: []u8, start_idx: int) -> (encode_size: int, ok: bool) {
|
|
out_size := 0
|
|
|
|
cur_idx := start_idx
|
|
iteration_max := 0
|
|
top: for cur_idx < len(packet) {
|
|
if packet[cur_idx] == 0 {
|
|
out_size += 1
|
|
break
|
|
}
|
|
|
|
if iteration_max > 255 {
|
|
return
|
|
}
|
|
|
|
if packet[cur_idx] > 63 && packet[cur_idx] != 0xC0 {
|
|
return
|
|
}
|
|
|
|
switch packet[cur_idx] {
|
|
case 0xC0:
|
|
out_size += 2
|
|
break top
|
|
case:
|
|
label_size := int(packet[cur_idx]) + 1
|
|
idx2 := cur_idx + label_size
|
|
|
|
if idx2 < cur_idx + 1 || idx2 > len(packet) {
|
|
return
|
|
}
|
|
|
|
out_size += label_size
|
|
cur_idx = idx2
|
|
}
|
|
|
|
iteration_max += 1
|
|
}
|
|
|
|
if start_idx + out_size > len(packet) {
|
|
return
|
|
}
|
|
|
|
return out_size, true
|
|
}
|
|
|
|
decode_hostname :: proc(packet: []u8, start_idx: int, allocator := context.allocator) -> (hostname: string, encode_size: int, ok: bool) {
|
|
output := [NAME_MAX]u8{}
|
|
b := strings.builder_from_slice(output[:])
|
|
|
|
// If you're on level 0, update out_bytes, everything through a pointer
|
|
// doesn't count towards this hostname's packet length
|
|
|
|
// Evaluate tokens to generate the hostname
|
|
out_size := 0
|
|
level := 0
|
|
print_size := 0
|
|
cur_idx := start_idx
|
|
iteration_max := 0
|
|
labels_added := 0
|
|
for cur_idx < len(packet) {
|
|
if packet[cur_idx] == 0 {
|
|
|
|
if (level == 0) {
|
|
out_size += 1
|
|
}
|
|
|
|
break
|
|
}
|
|
|
|
if iteration_max > 255 {
|
|
return
|
|
}
|
|
|
|
if packet[cur_idx] > 63 && packet[cur_idx] != 0xC0 {
|
|
return
|
|
}
|
|
|
|
switch packet[cur_idx] {
|
|
|
|
// This is a offset to more data in the packet, jump to it
|
|
case 0xC0:
|
|
pkt := packet[cur_idx:cur_idx+2]
|
|
val := (^u16be)(raw_data(pkt))^
|
|
offset := int(val & 0x3FFF)
|
|
if offset > len(packet) {
|
|
return
|
|
}
|
|
|
|
cur_idx = offset
|
|
|
|
if (level == 0) {
|
|
out_size += 2
|
|
level += 1
|
|
}
|
|
|
|
// This is a label, insert it into the hostname
|
|
case:
|
|
label_size := int(packet[cur_idx])
|
|
idx2 := cur_idx + label_size + 1
|
|
if idx2 < cur_idx + 1 || idx2 > len(packet) {
|
|
return
|
|
}
|
|
|
|
if print_size + label_size + 1 > NAME_MAX {
|
|
return
|
|
}
|
|
|
|
if labels_added > 0 {
|
|
strings.write_byte(&b, '.')
|
|
}
|
|
strings.write_bytes(&b, packet[cur_idx+1:idx2])
|
|
print_size += label_size + 1
|
|
labels_added += 1
|
|
|
|
cur_idx = idx2
|
|
|
|
if (level == 0) {
|
|
out_size += label_size + 1
|
|
}
|
|
}
|
|
|
|
iteration_max += 1
|
|
}
|
|
|
|
if start_idx + out_size > len(packet) {
|
|
return
|
|
}
|
|
|
|
return strings.clone(strings.to_string(b), allocator), out_size, true
|
|
}
|
|
|
|
// Uses RFC 952 & RFC 1123
|
|
validate_hostname :: proc(hostname: string) -> (ok: bool) {
|
|
if len(hostname) > 255 || len(hostname) == 0 {
|
|
return
|
|
}
|
|
|
|
if hostname[0] == '-' {
|
|
return
|
|
}
|
|
|
|
_hostname := hostname
|
|
for label in strings.split_iterator(&_hostname, ".") {
|
|
if len(label) > 63 || len(label) == 0 {
|
|
return
|
|
}
|
|
|
|
for ch in label {
|
|
switch ch {
|
|
case:
|
|
return
|
|
case 'a'..='z', 'A'..='Z', '0'..='9', '-':
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
parse_record :: proc(packet: []u8, cur_off: ^int, filter: DNS_Record_Type = nil) -> (record: DNS_Record, ok: bool) {
|
|
record_buf := packet[cur_off^:]
|
|
|
|
srv_record_name, hn_sz := decode_hostname(packet, cur_off^, context.temp_allocator) or_return
|
|
// TODO(tetra): Not sure what we should call this.
|
|
// Is it really only used in SRVs?
|
|
// Maybe some refactoring is required?
|
|
|
|
ahdr_sz := size_of(DNS_Record_Header)
|
|
if len(record_buf) - hn_sz < ahdr_sz {
|
|
return
|
|
}
|
|
|
|
record_hdr_bytes := record_buf[hn_sz:hn_sz+ahdr_sz]
|
|
record_hdr := cast(^DNS_Record_Header)raw_data(record_hdr_bytes)
|
|
|
|
data_sz := record_hdr.length
|
|
data_off := cur_off^ + int(hn_sz) + int(ahdr_sz)
|
|
data := packet[data_off:data_off+int(data_sz)]
|
|
cur_off^ += int(hn_sz) + int(ahdr_sz) + int(data_sz)
|
|
|
|
if u16be(filter) != record_hdr.type {
|
|
return nil, true
|
|
}
|
|
|
|
_record: DNS_Record
|
|
#partial switch DNS_Record_Type(record_hdr.type) {
|
|
case .IP4:
|
|
if len(data) != 4 {
|
|
return
|
|
}
|
|
|
|
addr := (^IP4_Address)(raw_data(data))^
|
|
|
|
_record = DNS_Record_IP4{
|
|
base = DNS_Record_Base{
|
|
record_name = strings.clone(srv_record_name),
|
|
ttl_seconds = u32(record_hdr.ttl),
|
|
},
|
|
address = addr,
|
|
}
|
|
|
|
case .IP6:
|
|
if len(data) != 16 {
|
|
return
|
|
}
|
|
|
|
addr := (^IP6_Address)(raw_data(data))^
|
|
|
|
_record = DNS_Record_IP6{
|
|
base = DNS_Record_Base{
|
|
record_name = strings.clone(srv_record_name),
|
|
ttl_seconds = u32(record_hdr.ttl),
|
|
},
|
|
address = addr,
|
|
}
|
|
|
|
case .CNAME:
|
|
hostname, _ := decode_hostname(packet, data_off) or_return
|
|
|
|
_record = DNS_Record_CNAME{
|
|
base = DNS_Record_Base{
|
|
record_name = strings.clone(srv_record_name),
|
|
ttl_seconds = u32(record_hdr.ttl),
|
|
},
|
|
host_name = hostname,
|
|
}
|
|
|
|
case .TXT:
|
|
_record = DNS_Record_TXT{
|
|
base = DNS_Record_Base{
|
|
record_name = strings.clone(srv_record_name),
|
|
ttl_seconds = u32(record_hdr.ttl),
|
|
},
|
|
value = strings.clone(string(data)),
|
|
}
|
|
|
|
case .NS:
|
|
name, _ := decode_hostname(packet, data_off) or_return
|
|
|
|
_record = DNS_Record_NS{
|
|
base = DNS_Record_Base{
|
|
record_name = strings.clone(srv_record_name),
|
|
ttl_seconds = u32(record_hdr.ttl),
|
|
},
|
|
host_name = name,
|
|
}
|
|
|
|
case .SRV:
|
|
if len(data) <= 6 {
|
|
return
|
|
}
|
|
|
|
_data := mem.slice_data_cast([]u16be, data)
|
|
|
|
priority, weight, port := _data[0], _data[1], _data[2]
|
|
target, _ := decode_hostname(packet, data_off + (size_of(u16be) * 3)) or_return
|
|
|
|
// NOTE(tetra): Srv record name should be of the form '_servicename._protocol.hostname'
|
|
// The record name is the name of the record.
|
|
// Not to be confused with the _target_ of the record, which is--in combination with the port--what we're looking up
|
|
// by making this request in the first place.
|
|
|
|
// NOTE(Jeroen): Service Name and Protocol Name can probably just be string slices into the record name.
|
|
// It's already cloned, after all. I wouldn't put them on the temp allocator like this.
|
|
|
|
parts := strings.split_n(srv_record_name, ".", 3, context.temp_allocator)
|
|
if len(parts) != 3 {
|
|
return
|
|
}
|
|
service_name, protocol_name := parts[0], parts[1]
|
|
|
|
_record = DNS_Record_SRV{
|
|
base = DNS_Record_Base{
|
|
record_name = strings.clone(srv_record_name),
|
|
ttl_seconds = u32(record_hdr.ttl),
|
|
},
|
|
target = target,
|
|
service_name = service_name,
|
|
protocol_name = protocol_name,
|
|
priority = int(priority),
|
|
weight = int(weight),
|
|
port = int(port),
|
|
}
|
|
|
|
case .MX:
|
|
if len(data) <= 2 {
|
|
return
|
|
}
|
|
|
|
preference: u16be = mem.slice_data_cast([]u16be, data)[0]
|
|
hostname, _ := decode_hostname(packet, data_off + size_of(u16be)) or_return
|
|
|
|
_record = DNS_Record_MX{
|
|
base = DNS_Record_Base{
|
|
record_name = strings.clone(srv_record_name),
|
|
ttl_seconds = u32(record_hdr.ttl),
|
|
},
|
|
host_name = hostname,
|
|
preference = int(preference),
|
|
}
|
|
|
|
case:
|
|
return
|
|
|
|
}
|
|
|
|
return _record, true
|
|
}
|
|
|
|
/*
|
|
DNS Query Response Format:
|
|
- DNS_Header (packed)
|
|
- Query Count
|
|
- Answer Count
|
|
- Authority Count
|
|
- Additional Count
|
|
- Query[]
|
|
- Hostname -- encoded
|
|
- Type
|
|
- Class
|
|
- Answer[]
|
|
- DNS Record Data
|
|
- Authority[]
|
|
- DNS Record Data
|
|
- Additional[]
|
|
- DNS Record Data
|
|
|
|
DNS Record Data:
|
|
- DNS_Record_Header
|
|
- Data[]
|
|
*/
|
|
|
|
parse_response :: proc(response: []u8, filter: DNS_Record_Type = nil, allocator := context.allocator) -> (records: []DNS_Record, ok: bool) {
|
|
context.allocator = allocator
|
|
|
|
HEADER_SIZE_BYTES :: 12
|
|
if len(response) < HEADER_SIZE_BYTES {
|
|
return
|
|
}
|
|
|
|
_records := make([dynamic]DNS_Record, 0)
|
|
|
|
dns_hdr_chunks := mem.slice_data_cast([]u16be, response[:HEADER_SIZE_BYTES])
|
|
hdr := unpack_dns_header(dns_hdr_chunks[0], dns_hdr_chunks[1])
|
|
if !hdr.is_response {
|
|
return
|
|
}
|
|
|
|
question_count := int(dns_hdr_chunks[2])
|
|
if question_count != 1 {
|
|
return
|
|
}
|
|
answer_count := int(dns_hdr_chunks[3])
|
|
authority_count := int(dns_hdr_chunks[4])
|
|
additional_count := int(dns_hdr_chunks[5])
|
|
|
|
cur_idx := HEADER_SIZE_BYTES
|
|
|
|
for _ in 0..<question_count {
|
|
if cur_idx == len(response) {
|
|
continue
|
|
}
|
|
|
|
dq_sz :: 4
|
|
hn_sz := skip_hostname(response, cur_idx) or_return
|
|
dns_query := mem.slice_data_cast([]u16be, response[cur_idx+hn_sz:cur_idx+hn_sz+dq_sz])
|
|
|
|
cur_idx += hn_sz + dq_sz
|
|
}
|
|
|
|
for _ in 0..<answer_count {
|
|
if cur_idx == len(response) {
|
|
continue
|
|
}
|
|
|
|
rec := parse_record(response, &cur_idx, filter) or_return
|
|
if rec != nil {
|
|
append(&_records, rec)
|
|
}
|
|
}
|
|
|
|
for _ in 0..<authority_count {
|
|
if cur_idx == len(response) {
|
|
continue
|
|
}
|
|
|
|
rec := parse_record(response, &cur_idx, filter) or_return
|
|
if rec != nil {
|
|
append(&_records, rec)
|
|
}
|
|
}
|
|
|
|
for _ in 0..<additional_count {
|
|
if cur_idx == len(response) {
|
|
continue
|
|
}
|
|
|
|
rec := parse_record(response, &cur_idx, filter) or_return
|
|
if rec != nil {
|
|
append(&_records, rec)
|
|
}
|
|
}
|
|
|
|
return _records[:], true
|
|
} |