3 -- Example NHRP events processing script which validates
4 -- NHRP registration GRE address against certificate subjectAltName IP
5 -- and auto-creates BGP pairings and filters based on sbgp extensions.
7 -- Depends on lua5.2 lua5.2-posix lua5.2-cqueues lua5.2-ossl lua-asn1
9 local posix = require 'posix'
10 local struct = require 'struct'
11 local cq = require 'cqueues'
12 local cqs = require 'cqueues.socket'
13 local x509 = require 'openssl.x509'
14 local x509an = require 'openssl.x509.altname'
15 local rfc3779 = require 'asn1.rfc3779'
17 local SOCK = "/var/run/nhrp-events.sock"
21 local nulfd = posix.open("/dev/null", posix.O_RDWR)
22 local listener = cqs.listen{path=SOCK}
24 posix.chown(SOCK, "quagga", "quagga")
25 posix.setpid("u", "quagga")
26 posix.setpid("g", "quagga")
27 posix.openlog("nhrp-events", "np")
29 function string.hex2bin(str)
30 return str:gsub('..', function(cc) return string.char(tonumber(cc, 16)) end)
33 local function decode_ext(cert, name, tpe)
34 local ext = cert:getExtension(name)
35 if not ext then return end
36 return tpe.decode(ext:getData())
39 local function do_parse_cert(cert, out)
40 for type, value in pairs(cert:getSubjectAlt()) do
42 table.insert(out.GRE, value)
45 if #out.GRE == 0 then return end
47 local asn = decode_ext(cert, 'sbgp-autonomousSysNum', rfc3779.ASIdentifiers)
48 if asn and asn.asnum and asn.asnum.asIdsOrRanges then
49 for _, as in ipairs(asn.asnum.asIdsOrRanges) do
51 out.AS = tonumber(as.id)
57 local addrBlocks = decode_ext(cert, 'sbgp-ipAddrBlock', rfc3779.IPAddrBlocks)
58 for _, ab in ipairs(addrBlocks or {}) do
59 if ab.ipAddressChoice and ab.ipAddressChoice.addressesOrRanges then
60 for _, a in ipairs(ab.ipAddressChoice.addressesOrRanges) do
61 if a.addressPrefix then
62 table.insert(out.NET, a.addressPrefix)
71 local function parse_cert(certhex)
78 local cert = x509.new(certhex:hex2bin(), 'der')
79 out.cn = tostring(cert:getSubject())
80 -- Recognize hubs by certificate's CN to have OU=Hubs
81 out.hub = out.cn:match("/OU=Hubs/") and true or nil
82 do_parse_cert(cert, out)
86 local function execute(desc, cmd, ...)
87 local piper, pipew = posix.pipe()
89 return error("Pipe failed")
92 local pid = posix.fork()
94 return error("Fork failed")
101 posix.execp(cmd, ...)
106 -- This blocks -- perhaps should handle command executions in separate queue.
109 local d = posix.read(piper, 8192)
110 if d == nil or d == "" then break end
111 table.insert(output, d)
115 local _, reason, status = posix.wait(pid)
117 posix.syslog(6, ("Executed '%s' successfully"):format(desc))
119 posix.syslog(3, ("Failed to execute '%s': %s %d"):format(desc, reason, status))
121 return status, table.concat(output)
124 local function configure_bgp(desc, ...)
127 "-c", "configure terminal",
129 for _, val in ipairs({...}) do
130 table.insert(args, "-c")
131 table.insert(args, val)
133 return execute(desc, "vtysh", table.unpack(args))
136 local last_bgp_reset = 0
138 local function bgp_reset(msg, local_cert)
139 local now = os.time()
140 if last_bgp_reset + 60 > now then return end
143 configure_bgp("spoke reset",
144 "route-map RTT-SET permit 10", "set metric rtt", "exit",
145 "route-map RTT-ADD permit 10", "set metric +rtt", "exit",
146 ("router bgp %d"):format(local_cert.AS),
148 "neighbor hubs peer-group",
149 "neighbor hubs remote-as 65000",
150 "neighbor hubs ebgp-multihop 1",
151 "neighbor hubs disable-connected-check",
152 "neighbor hubs timers 10 30",
153 "neighbor hubs timers connect 10",
154 "neighbor hubs next-hop-self all",
155 "neighbor hubs soft-reconfiguration inbound",
156 "neighbor hubs route-map RTT-ADD in")
159 local function bgp_nhs_up(msg, remote_cert, local_cert)
160 configure_bgp(("nhs-up %s"):format(msg.remote_addr),
161 ("router bgp %s"):format(local_cert.AS),
162 ("neighbor %s peer-group hubs"):format(msg.remote_addr))
165 local function bgp_nhs_down(msg, remote_cert, local_cert)
166 configure_bgp(("nhs-down %s"):format(msg.remote_addr),
167 ("router bgp %s"):format(local_cert.AS),
168 ("no neighbor %s"):format(msg.remote_addr))
171 local function bgp_create_spoke_rules(msg, remote_cert, local_cert)
172 if not local_cert.hub then return end
175 for seq, net in ipairs(remote_cert.NET) do
177 ("ip prefix-list net-%s-in seq %d permit %s le %d"):format(
178 msg.remote_addr, seq * 5, net,
179 remote_cert.hub and 32 or 26))
181 table.insert(bgpcfg, ("router bgp %s"):format(local_cert.AS))
182 if remote_cert.hub then
183 table.insert(bgpcfg, ("neighbor %s peer-group hubs"):format(msg.remote_addr))
184 elseif local_cert.AS == remote_cert.AS then
185 table.insert(bgpcfg, ("neighbor %s peer-group spoke-ibgp"):format(msg.remote_addr))
187 table.insert(bgpcfg, ("neighbor %s remote-as %s"):format(msg.remote_addr, remote_cert.AS))
188 table.insert(bgpcfg, ("neighbor %s peer-group spoke-ebgp"):format(msg.remote_addr))
190 table.insert(bgpcfg, ("neighbor %s prefix-list net-%s-in in"):format(msg.remote_addr, msg.remote_addr))
192 local status, output = configure_bgp(("nhc-register %s"):format(msg.remote_addr), table.unpack(bgpcfg))
193 if output:find("Cannot") then
194 posix.syslog(6, "BGP: "..output)
196 ("nhc-recreate %s"):format(msg.remote_addr),
197 ("router bgp %s"):format(local_cert.AS),
198 ("no neighbor %s"):format(msg.remote_addr),
199 table.unpack(bgpcfg))
203 local function handle_message(msg)
204 if msg.event ~= "authorize-binding" then return end
206 -- Verify protocol address against certificate
208 local local_cert = parse_cert(msg.local_cert)
209 local remote_cert = parse_cert(msg.remote_cert)
210 for _, gre in pairs(remote_cert.GRE) do
211 if gre == msg.remote_addr then auth = true end
214 posix.syslog(3, ("GRE %s to NBMA %s DENIED (cert '%s', allows: %s)"):format(
215 msg.remote_addr, msg.remote_nbma,
216 remote_cert.cn, table.concat(remote_cert.GRE, " ")))
219 posix.syslog(6, ("GRE %s to NBMA %s authenticated for %s"):format(
220 msg.remote_addr, msg.remote_nbma, remote_cert.cn))
222 -- Automatic BGP binding for hub-spoke connections
223 if msg.type == "nhs" and msg.old_type ~= "nhs" then
224 if not local_cert.hub then
225 if tonumber(msg.num_nhs) == 0 and msg.vc_initiated == "yes" then
226 bgp_reset(msg, local_cert)
228 bgp_nhs_up(msg, remote_cert, local_cert)
230 bgp_create_spoke_rules(msg, remote_cert, local_cert)
232 elseif msg.type ~= "nhs" and msg.old_type == "nhs" then
233 bgp_nhs_down(msg, remote_cert, local_cert)
234 elseif msg.type == "dynamic" and msg.old_type ~= "dynamic" then
235 bgp_create_spoke_rules(msg, remote_cert, local_cert)
241 local function handle_connection(conn)
243 for l in conn:lines() do
245 res = handle_message(msg)
247 conn:write(("eventid=%s\nresult=%s\n\n"):format(msg.eventid, res or "default"))
251 local key, value = l:match('([^=]*)=(.*)')
252 if key and value then
262 local conn = listener:accept()
263 conn:setmode("b", "bl")
265 local ok, msg = pcall(handle_connection, conn)
266 if not ok then posix.syslog(3, msg) end