dns.wire — Lilush API

←index

← dns

Overview

Cursor-based binary reader and builder-based writer for DNS wire format. Handles name compression (RFC1035 §4.1.4) in both directions: the reader resolves compression pointers when decoding names, and the writer builds a compression table to emit pointers for previously-seen domain name suffixes.

The reader wraps a raw binary string with a 1-based cursor. Every read advances the cursor. The writer accumulates bytes into a flat table and converts to a string on output(). Both enforce RFC1035 security constraints on label length, total name length, and pointer validity.

Functions

NameSignature
reader:u8reader:u8() -> val, err
reader:u16reader:u16() -> val, err
reader:u32reader:u32() -> val, err
reader:bytesreader:bytes(n) -> data, err
reader:posreader:pos() -> position
reader:remainingreader:remaining() -> count
reader:seekreader:seek(pos)
reader:skipreader:skip(n)
reader:at_endreader:at_end() -> ended
reader:namereader:name() -> fqdn, err
reader:character_stringreader:character_string() -> data, err
reader:sub_readerreader:sub_reader(n) -> reader, err
readerreader(raw) -> reader
writer:u8writer:u8(val)
writer:u16writer:u16(val)
writer:u32writer:u32(val)
writer:byteswriter:bytes(data)
writer:poswriter:pos() -> offset
writer:lenwriter:len() -> length
writer:outputwriter:output() -> data
writer:patch_u16writer:patch_u16(offset, val)
writer:namewriter:name(domain)
writer:character_stringwriter:character_string(data)
writerwriter() -> writer

reader:u8() -> val, err

Read a single unsigned byte

reader:u16() -> val, err

Read a 2-byte big-endian unsigned integer

reader:u32() -> val, err

Read a 4-byte big-endian unsigned integer

reader:bytes(n) -> data, err

Read exactly n bytes as a string

reader:pos() -> position

Return the current 1-based cursor position

reader:remaining() -> count

Return the number of bytes remaining from cursor to end

reader:seek(pos)

Seek to an absolute 1-based position

reader:skip(n)

Advance the cursor by n bytes

reader:at_end() -> ended

Return true if the cursor is at or past the end of data

reader:name() -> fqdn, err

Read a DNS compressed name, returning the fully qualified domain name

Reads a DNS name from the wire, handling compression pointers per RFC1035 §4.1.4. Returns the name with a trailing dot (e.g. "example.com."). The root name (single zero byte) returns ".".

Security constraints enforced:

For sub_readers: pointer chasing reads from the full parent message buffer, allowing resolution of pointers outside the sub_reader's bounds.

reader:character_string() -> data, err

Read a character-string (length-prefixed per RFC1035 §3.3)

reader:sub_reader(n) -> reader, err

Create a bounded sub-reader over the next n bytes

Shares the parent's full message buffer for compression pointer resolution. Parent cursor advances past the bounded region. Used for RDATA parsing where each record's RDATA must be read from exactly n bytes (the RDLENGTH field).

reader(raw) -> reader

Create a wire format reader from a raw binary string

Returns a cursor-based reader wrapping the given binary string. The reader provides methods for reading DNS wire format primitives (u8, u16, u32, bytes), compressed names, character-strings, and position management (pos, remaining, seek, skip, at_end).

Use sub_reader(n) to create bounded readers for RDATA parsing. Sub-readers share the parent's message buffer so compression pointers still resolve correctly.

writer:u8(val)

Write a single unsigned byte

writer:u16(val)

Write a 2-byte big-endian unsigned integer

writer:u32(val)

Write a 4-byte big-endian unsigned integer

writer:bytes(data)

Write raw bytes from a string

writer:pos() -> offset

Return the current 0-based write offset

writer:len() -> length

Return the current length in bytes

writer:output() -> data

Return the accumulated bytes as a string

Converts the internal byte table to a binary string. Uses batched string.char(unpack(...)) to avoid LuaJIT stack limits on large messages.

writer:patch_u16(offset, val)

Overwrite 2 bytes at a 0-based offset (for header fixups)

Patches 2 bytes at the given 0-based wire offset. Typically used to fill in section counts in the DNS header after all sections have been written.

writer:name(domain)

Write a DNS name with compression

Writes a DNS domain name to the wire, using compression pointers for previously-seen suffixes. The domain should include a trailing dot (e.g. "example.com."). If omitted, one is appended.

The writer maintains a compression table mapping FQDN suffixes to their 0-based wire offsets. When a suffix has been seen before, a 2-byte pointer is emitted instead of repeating the labels.

Validation:

writer:character_string(data)

Write a character-string (length-prefixed per RFC1035 §3.3)

writer() -> writer

Create a new wire format writer

Returns a builder-based writer that accumulates bytes into an internal table. Provides methods for writing DNS wire format primitives (u8, u16, u32, bytes), compressed names, and character-strings. Use output() to get the accumulated bytes as a string. Use patch_u16(offset, val) for header fixups.

The writer maintains a compression table for name compression: when writing the same domain name suffix twice, the second occurrence is replaced with a 2-byte pointer.