Browse Source

nhrpd: add example nhrp event processing script (in lua)

Timo Teräs 2 years ago
parent
commit
1d00f7e16b
1 changed files with 272 additions and 0 deletions
  1. 272 0
      nhrpd/nhrp-events.lua

+ 272 - 0
nhrpd/nhrp-events.lua

@@ -0,0 +1,272 @@
+#!/usr/bin/lua5.2
+
+-- Example NHRP events processing script which validates
+-- NHRP registration GRE address against certificate subjectAltName IP
+-- and auto-creates BGP pairings and filters based on sbgp extensions.
+
+-- Depends on lua5.2 lua5.2-posix lua5.2-cqueues lua5.2-ossl lua-asn1
+
+local posix = require 'posix'
+local struct = require 'struct'
+local cq = require 'cqueues'
+local cqs = require 'cqueues.socket'
+local x509 = require 'openssl.x509'
+local x509an = require 'openssl.x509.altname'
+local rfc3779 = require 'asn1.rfc3779'
+
+local SOCK = "/var/run/nhrp-events.sock"
+posix.unlink(SOCK)
+
+local loop = cq.new()
+local nulfd = posix.open("/dev/null", posix.O_RDWR)
+local listener = cqs.listen{path=SOCK}
+
+posix.chown(SOCK, "quagga", "quagga")
+posix.setpid("u", "quagga")
+posix.setpid("g", "quagga")
+posix.openlog("nhrp-events", "np")
+
+function string.hex2bin(str)
+	return str:gsub('..', function(cc) return string.char(tonumber(cc, 16)) end)
+end
+
+local function decode_ext(cert, name, tpe)
+	local ext = cert:getExtension(name)
+	if not ext then return end
+	return tpe.decode(ext:getData())
+end
+
+local function do_parse_cert(cert, out)
+	for type, value in pairs(cert:getSubjectAlt()) do
+		if type == 'IP' then
+			table.insert(out.GRE, value)
+		end
+	end
+	if #out.GRE == 0 then return end
+
+	local asn = decode_ext(cert, 'sbgp-autonomousSysNum', rfc3779.ASIdentifiers)
+	if asn and asn.asnum and asn.asnum.asIdsOrRanges then
+		for _, as in ipairs(asn.asnum.asIdsOrRanges) do
+			if as.id then
+				out.AS = tonumber(as.id)
+				break
+			end
+		end
+	end
+
+	local addrBlocks = decode_ext(cert, 'sbgp-ipAddrBlock', rfc3779.IPAddrBlocks)
+	for _, ab in ipairs(addrBlocks or {}) do
+		if ab.ipAddressChoice and ab.ipAddressChoice.addressesOrRanges then
+			for _, a in ipairs(ab.ipAddressChoice.addressesOrRanges) do
+				if a.addressPrefix then
+					table.insert(out.NET, a.addressPrefix)
+				end
+			end
+		end
+	end
+
+	return true
+end
+
+local function parse_cert(certhex)
+	local out = {
+		cn = "(no CN)",
+		AS = 0,
+		GRE = {},
+		NET = {},
+	}
+	local cert = x509.new(certhex:hex2bin(), 'der')
+	out.cn = tostring(cert:getSubject())
+	-- Recognize hubs by certificate's CN to have OU=Hubs
+	out.hub = out.cn:match("/OU=Hubs/") and true or nil
+	do_parse_cert(cert, out)
+	return out
+end
+
+local function execute(desc, cmd, ...)
+	local piper, pipew = posix.pipe()
+	if piper == nil then
+		return error("Pipe failed")
+	end
+
+	local pid = posix.fork()
+	if pid == -1 then
+		return error("Fork failed")
+	end
+	if pid == 0 then
+		posix.close(piper)
+		posix.dup2(nulfd, 0)
+		posix.dup2(pipew, 1)
+		posix.dup2(nulfd, 2)
+		posix.execp(cmd, ...)
+		os.exit(1)
+	end
+	posix.close(pipew)
+
+	-- This blocks -- perhaps should handle command executions in separate queue.
+	local output = {}
+	while true do
+		local d = posix.read(piper, 8192)
+		if d == nil or d == "" then break end
+		table.insert(output, d)
+	end
+	posix.close(piper)
+
+	local _, reason, status = posix.wait(pid)
+	if status == 0 then
+		posix.syslog(6, ("Executed '%s' successfully"):format(desc))
+	else
+		posix.syslog(3, ("Failed to execute '%s': %s %d"):format(desc, reason, status))
+	end
+	return status, table.concat(output)
+end
+
+local function configure_bgp(desc, ...)
+	local args = {
+		"-d", "bgpd",
+		"-c", "configure terminal",
+	}
+	for _, val in ipairs({...}) do
+		table.insert(args, "-c")
+		table.insert(args, val)
+	end
+	return execute(desc, "vtysh", table.unpack(args))
+end
+
+local last_bgp_reset = 0
+
+local function bgp_reset(msg, local_cert)
+	local now = os.time()
+	if last_bgp_reset + 60 > now then return end
+	last_bgp_reset = now
+
+	configure_bgp("spoke reset",
+		"route-map RTT-SET permit 10", "set metric rtt", "exit",
+		"route-map RTT-ADD permit 10", "set metric +rtt", "exit",
+		("router bgp %d"):format(local_cert.AS),
+		"no neighbor hubs",
+		"neighbor hubs peer-group",
+		"neighbor hubs remote-as 65000",
+		"neighbor hubs ebgp-multihop 1",
+		"neighbor hubs disable-connected-check",
+		"neighbor hubs timers 10 30",
+		"neighbor hubs timers connect 10",
+		"neighbor hubs next-hop-self all",
+		"neighbor hubs soft-reconfiguration inbound",
+		"neighbor hubs route-map RTT-ADD in")
+end
+
+local function bgp_nhs_up(msg, remote_cert, local_cert)
+	configure_bgp(("nhs-up %s"):format(msg.remote_addr),
+		("router bgp %s"):format(local_cert.AS),
+		("neighbor %s peer-group hubs"):format(msg.remote_addr))
+end
+
+local function bgp_nhs_down(msg, remote_cert, local_cert)
+	configure_bgp(("nhs-down %s"):format(msg.remote_addr),
+		("router bgp %s"):format(local_cert.AS),
+		("no neighbor %s"):format(msg.remote_addr))
+end
+
+local function bgp_create_spoke_rules(msg, remote_cert, local_cert)
+	if not local_cert.hub then return end
+
+	local bgpcfg = {}
+	for seq, net in ipairs(remote_cert.NET) do
+		table.insert(bgpcfg,
+			("ip prefix-list net-%s-in seq %d permit %s le %d"):format(
+				msg.remote_addr, seq * 5, net,
+				remote_cert.hub and 32 or 26))
+	end
+	table.insert(bgpcfg, ("router bgp %s"):format(local_cert.AS))
+	if remote_cert.hub then
+		table.insert(bgpcfg, ("neighbor %s peer-group hubs"):format(msg.remote_addr))
+	elseif local_cert.AS == remote_cert.AS then
+		table.insert(bgpcfg, ("neighbor %s peer-group spoke-ibgp"):format(msg.remote_addr))
+	else
+		table.insert(bgpcfg, ("neighbor %s remote-as %s"):format(msg.remote_addr, remote_cert.AS))
+		table.insert(bgpcfg, ("neighbor %s peer-group spoke-ebgp"):format(msg.remote_addr))
+	end
+	table.insert(bgpcfg, ("neighbor %s prefix-list net-%s-in in"):format(msg.remote_addr, msg.remote_addr))
+
+	local status, output = configure_bgp(("nhc-register %s"):format(msg.remote_addr), table.unpack(bgpcfg))
+	if output:find("Cannot") then
+		posix.syslog(6, "BGP: "..output)
+		configure_bgp(
+			("nhc-recreate %s"):format(msg.remote_addr),
+			("router bgp %s"):format(local_cert.AS),
+			("no neighbor %s"):format(msg.remote_addr),
+			table.unpack(bgpcfg))
+	end
+end
+
+local function handle_message(msg)
+	if msg.event ~= "authorize-binding" then return end
+
+	-- Verify protocol address against certificate
+	local auth = false
+	local local_cert = parse_cert(msg.local_cert)
+	local remote_cert = parse_cert(msg.remote_cert)
+	for _, gre in pairs(remote_cert.GRE) do
+		if gre == msg.remote_addr then auth = true end
+	end
+	if not auth then
+		posix.syslog(3, ("GRE %s to NBMA %s DENIED (cert '%s', allows: %s)"):format(
+			msg.remote_addr, msg.remote_nbma,
+			remote_cert.cn, table.concat(remote_cert.GRE, " ")))
+		return "deny"
+	end
+	posix.syslog(6, ("GRE %s to NBMA %s authenticated for %s"):format(
+		msg.remote_addr, msg.remote_nbma, remote_cert.cn))
+
+	-- Automatic BGP binding for hub-spoke connections
+	if msg.type == "nhs" and msg.old_type ~= "nhs" then
+		if not local_cert.hub then
+			if tonumber(msg.num_nhs) == 0 and msg.vc_initiated == "yes" then
+				bgp_reset(msg, local_cert)
+			end
+			bgp_nhs_up(msg, remote_cert, local_cert)
+		else
+			bgp_create_spoke_rules(msg, remote_cert, local_cert)
+		end
+	elseif msg.type ~= "nhs" and msg.old_type == "nhs" then
+		bgp_nhs_down(msg, remote_cert, local_cert)
+	elseif msg.type == "dynamic" and msg.old_type ~= "dynamic" then
+		bgp_create_spoke_rules(msg, remote_cert, local_cert)
+	end
+
+	return "accept"
+end
+
+local function handle_connection(conn)
+	local msg = {}
+	for l in conn:lines() do
+		if l == "" then
+			res = handle_message(msg)
+			if msg.eventid then
+				conn:write(("eventid=%s\nresult=%s\n\n"):format(msg.eventid, res or "default"))
+			end
+			msg = {}
+		else
+			local key, value = l:match('([^=]*)=(.*)')
+			if key and value then
+				msg[key] = value
+			end
+		end
+	end
+	conn:close()
+end
+
+loop:wrap(function()
+	while true do
+		local conn = listener:accept()
+		conn:setmode("b", "bl")
+		loop:wrap(function()
+			local ok, msg = pcall(handle_connection, conn)
+			if not ok then posix.syslog(3, msg) end
+			conn:close()
+		end)
+	end
+end)
+
+print(loop:loop())