DNS Zone Parsers with LPeg in Lua
Parse BIND9 zone files with LPeg in Lua.
tl;dr
To parse DNS zone files (RFC 1035 #5.1) with LPeg:
local lpeg = require('lpeg')
local LOWER = lpeg.R('az')
local UPPER = lpeg.R('AZ')
local LETTER = LOWER + UPPER
local DIGIT = lpeg.R('09')
local NEWLINE = lpeg.P('\n')
local INLINESPACE = lpeg.S('\t ')
local ALLSPACE = NEWLINE + INLINESPACE
local INLINESEP = INLINESPACE ^ 1
local LINESEP = (ALLSPACE ^ 0) * NEWLINE * (ALLSPACE ^ 0)
local INTEGER = DIGIT ^ 1
local DOLLAR = lpeg.P('$')
local DIRECTIVE = DOLLAR * lpeg.C(LETTER ^ 0)
local UNDERSCORE = lpeg.P('_')
local HYPHEN = lpeg.P('-')
local DOT = lpeg.P('.')
local AT = lpeg.P('@')
local STAR = lpeg.P('*')
-- Match a domain
local FullyQualified, Relative, DomainPart = lpeg.V('FullyQualified'), lpeg.V('Relative'), lpeg.V('DomainPart')
local Domain = lpeg.P{
'Domain',
Domain = lpeg.C(AT + FullyQualified + Relative),
FullyQualified = Relative ^ -1 * DOT,
Relative = (DomainPart + STAR) * (DOT * DomainPart) ^ 0,
DomainPart = (LETTER + DIGIT + HYPHEN + UNDERSCORE) ^ 1,
}
-- Match a record
local SEMICOLON = lpeg.P(';')
local COMMENT = SEMICOLON * (lpeg.P(1) - NEWLINE) ^ 0
local OPEN = lpeg.P('(')
local CLOSE = lpeg.P(')')
local Name, TTL, Class, Type, RDATA = lpeg.V('Name'), lpeg.V('TTL'), lpeg.V('Class'), lpeg.V('Type'), lpeg.V('RDATA')
local NONEND = lpeg.P(1) - NEWLINE - SEMICOLON - OPEN
local Line = NONEND ^ 0 * (OPEN * (lpeg.P(1) - CLOSE) ^ 0 * CLOSE) ^ -1
local Directive = DIRECTIVE * Line
local Record = lpeg.P{
'Record',
Record = lpeg.Ct(
Name * (
Class * TTL ^ -1 +
TTL * Class ^ -1
) ^ -1 * Type * RDATA
),
Name = lpeg.Cg(Domain ^ -1, 'name') * INLINESEP,
TTL = lpeg.Cg(INTEGER, 'ttl') * INLINESEP,
Class = lpeg.Cg(lpeg.P('IN') + lpeg.P('CH') + lpeg.P('HS'), 'class') * INLINESEP,
Type = lpeg.Cg(LETTER ^ 1, 'type') * INLINESEP,
RDATA = lpeg.Cg(Line, 'data')
}
local Directive = lpeg.Ct(lpeg.Cg(DIRECTIVE * Line, 'directive'))
local Entry = lpeg.V('Entry')
local Empty = lpeg.V('Empty')
local Entries = lpeg.V('Entries')
local Zone = lpeg.P{
'Zone',
Zone = Entries,
Entries = lpeg.Ct(
Entry ^ -1 * (NEWLINE * Entry) ^ 0
),
Empty = INLINESEP ^ -1,
Entry = (Directive + Record) ^ -1 * Empty * (COMMENT ^ -1),
}
local function encode(zone)
local result = ''
for _, e in ipairs(zone) do
if e.directive then
result = result .. e.directive .. '\n'
else
local line = ''
line = line .. e.name .. '\t'
if e.ttl then line = line .. e.ttl .. '\t' end
if e.class then line = line .. e.class .. '\t' end
line = line .. e.type .. '\t'
line = line .. e.data .. '\n'
result = result .. line
end
end
return result
end
return {
Zone = Zone,
Domain = Domain,
encode = encode,
decode = function(content) return lpeg.match(Zone, content) end
}
Usage
local zone = require('zone')
local inspect = require('inspect')
local result = zone.decode[[
; COMMENT1
. 3600000 NS A.ROOT-SERVERS.NET. ; COMMENT2
A.ROOT-SERVERS.NET. 3600000 A 198.41.0.4
A.ROOT-SERVERS.NET. 3600000 AAAA 2001:503:ba3e::2:30
. 3600000 NS B.ROOT-SERVERS.NET.
B.ROOT-SERVERS.NET. 3600000 A 192.228.79.201
B.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:84::b
. 3600000 NS C.ROOT-SERVERS.NET.
C.ROOT-SERVERS.NET. 3600000 A 192.33.4.12
C.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:2::c
; COMMENT3
A 1.1.1.1
TEST0.TEST. A 1.1.1.1
CLASS0.TEST. IN A 1.1.1.1
CLASS1.TEST. 66 IN A 1.1.1.1
CLASS2.TEST. IN 33 A 1.1.1.1
]]
print(inspect(result))
--[[
Output:
{ {
data = "A.ROOT-SERVERS.NET. ",
name = ".",
ttl = "3600000",
type = "NS"
}, {
data = "198.41.0.4",
name = "A.ROOT-SERVERS.NET.",
ttl = "3600000",
type = "A"
}, {
data = "2001:503:ba3e::2:30",
name = "A.ROOT-SERVERS.NET.",
ttl = "3600000",
type = "AAAA"
}, {
data = "B.ROOT-SERVERS.NET.",
name = ".",
ttl = "3600000",
type = "NS"
}, {
data = "192.228.79.201",
name = "B.ROOT-SERVERS.NET.",
ttl = "3600000",
type = "A"
}, {
data = "2001:500:84::b",
name = "B.ROOT-SERVERS.NET.",
ttl = "3600000",
type = "AAAA"
}, {
data = "C.ROOT-SERVERS.NET.",
name = ".",
ttl = "3600000",
type = "NS"
}, {
data = "192.33.4.12",
name = "C.ROOT-SERVERS.NET.",
ttl = "3600000",
type = "A"
}, {
data = "2001:500:2::c",
name = "C.ROOT-SERVERS.NET.",
ttl = "3600000",
type = "AAAA"
}, {
data = "1.1.1.1",
name = "",
type = "A"
}, {
data = "1.1.1.1",
name = "TEST0.TEST.",
type = "A"
}, {
class = "IN",
data = "1.1.1.1",
name = "CLASS0.TEST.",
type = "A"
}, {
class = "IN",
data = "1.1.1.1",
name = "CLASS1.TEST.",
ttl = "66",
type = "A"
}, {
class = "IN",
data = "1.1.1.1",
name = "CLASS2.TEST.",
ttl = "33",
type = "A"
} }
]]