+++ /dev/null
-#!/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())