#!/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 S-BGP extensions. Copyright (c) 2015-2017 Timo Teräs Copyright (c) 2017-2018 Kaarle Ritvanen See LICENSE file for license details ]]-- 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 dmvpn = require 'dmvpn' 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) local ext = cert:getExtension(name) if not ext then return end return dmvpn.decode_ext(name, ext) 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 out.AS = decode_ext(cert, 'sbgp-autonomousSysNum') out.NET = decode_ext(cert, 'sbgp-ipAddrBlock') 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()) out.hub = decode_ext(cert, dmvpn.OID_IS_HUB) 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-ADD permit 10", "set metric +rtt", "exit", ("router bgp %d"):format(local_cert.AS), "no neighbor hubs", "neighbor hubs peer-group", "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) local bgpcfg = { ("router bgp %s"):format(local_cert.AS), ("neighbor %s remote-as %d"):format(msg.remote_addr, remote_cert.AS), ("neighbor %s peer-group hubs"):format(msg.remote_addr) } if #local_cert.NET[2] > 0 then table.insert(bgpcfg, "address-family ipv6") table.insert( bgpcfg, ("neighbor %s activate"):format(msg.remote_addr) ) end configure_bgp( ("nhs-up %s"):format(msg.remote_addr), table.unpack(bgpcfg) ) 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 if not config then local conf_file = io.open("/etc/nhrp-events.conf") config = require("lyaml").load(conf_file:read("*a")) conf_file:close() end local bgpcfg = {} for afi, family in ipairs{"ip", "ipv6"} do for seq, net in ipairs(remote_cert.NET[afi]) do local len = tonumber(net:match('/(%d+)$')) local limit = remote_cert.hub and ({32, 128})[afi] or config['max-prefix-length'][family] table.insert( bgpcfg, ("%s prefix-list net-%s-in seq %d permit %s%s"):format( family, msg.remote_addr, seq * 5, net, limit > len and (" le %d"):format(limit) or "" ) ) end 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)) if #remote_cert.NET[2] > 0 then table.insert(bgpcfg, "address-family ipv6") table.insert( bgpcfg, ("neighbor %s activate"):format(msg.remote_addr) ) table.insert( bgpcfg, ("neighbor %s prefix-list net-%s-in in"):format(msg.remote_addr, msg.remote_addr) ) end 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.remote_addr:find("%.") then 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 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())