nhrp-events.lua 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. #!/usr/bin/lua5.2
  2. -- Example NHRP events processing script which validates
  3. -- NHRP registration GRE address against certificate subjectAltName IP
  4. -- and auto-creates BGP pairings and filters based on sbgp extensions.
  5. -- Depends on lua5.2 lua5.2-posix lua5.2-cqueues lua5.2-ossl lua-asn1
  6. local posix = require 'posix'
  7. local struct = require 'struct'
  8. local cq = require 'cqueues'
  9. local cqs = require 'cqueues.socket'
  10. local x509 = require 'openssl.x509'
  11. local x509an = require 'openssl.x509.altname'
  12. local rfc3779 = require 'asn1.rfc3779'
  13. local SOCK = "/var/run/nhrp-events.sock"
  14. posix.unlink(SOCK)
  15. local loop = cq.new()
  16. local nulfd = posix.open("/dev/null", posix.O_RDWR)
  17. local listener = cqs.listen{path=SOCK}
  18. posix.chown(SOCK, "quagga", "quagga")
  19. posix.setpid("u", "quagga")
  20. posix.setpid("g", "quagga")
  21. posix.openlog("nhrp-events", "np")
  22. function string.hex2bin(str)
  23. return str:gsub('..', function(cc) return string.char(tonumber(cc, 16)) end)
  24. end
  25. local function decode_ext(cert, name, tpe)
  26. local ext = cert:getExtension(name)
  27. if not ext then return end
  28. return tpe.decode(ext:getData())
  29. end
  30. local function do_parse_cert(cert, out)
  31. for type, value in pairs(cert:getSubjectAlt()) do
  32. if type == 'IP' then
  33. table.insert(out.GRE, value)
  34. end
  35. end
  36. if #out.GRE == 0 then return end
  37. local asn = decode_ext(cert, 'sbgp-autonomousSysNum', rfc3779.ASIdentifiers)
  38. if asn and asn.asnum and asn.asnum.asIdsOrRanges then
  39. for _, as in ipairs(asn.asnum.asIdsOrRanges) do
  40. if as.id then
  41. out.AS = tonumber(as.id)
  42. break
  43. end
  44. end
  45. end
  46. local addrBlocks = decode_ext(cert, 'sbgp-ipAddrBlock', rfc3779.IPAddrBlocks)
  47. for _, ab in ipairs(addrBlocks or {}) do
  48. if ab.ipAddressChoice and ab.ipAddressChoice.addressesOrRanges then
  49. for _, a in ipairs(ab.ipAddressChoice.addressesOrRanges) do
  50. if a.addressPrefix then
  51. table.insert(out.NET, a.addressPrefix)
  52. end
  53. end
  54. end
  55. end
  56. return true
  57. end
  58. local function parse_cert(certhex)
  59. local out = {
  60. cn = "(no CN)",
  61. AS = 0,
  62. GRE = {},
  63. NET = {},
  64. }
  65. local cert = x509.new(certhex:hex2bin(), 'der')
  66. out.cn = tostring(cert:getSubject())
  67. -- Recognize hubs by certificate's CN to have OU=Hubs
  68. out.hub = out.cn:match("/OU=Hubs/") and true or nil
  69. do_parse_cert(cert, out)
  70. return out
  71. end
  72. local function execute(desc, cmd, ...)
  73. local piper, pipew = posix.pipe()
  74. if piper == nil then
  75. return error("Pipe failed")
  76. end
  77. local pid = posix.fork()
  78. if pid == -1 then
  79. return error("Fork failed")
  80. end
  81. if pid == 0 then
  82. posix.close(piper)
  83. posix.dup2(nulfd, 0)
  84. posix.dup2(pipew, 1)
  85. posix.dup2(nulfd, 2)
  86. posix.execp(cmd, ...)
  87. os.exit(1)
  88. end
  89. posix.close(pipew)
  90. -- This blocks -- perhaps should handle command executions in separate queue.
  91. local output = {}
  92. while true do
  93. local d = posix.read(piper, 8192)
  94. if d == nil or d == "" then break end
  95. table.insert(output, d)
  96. end
  97. posix.close(piper)
  98. local _, reason, status = posix.wait(pid)
  99. if status == 0 then
  100. posix.syslog(6, ("Executed '%s' successfully"):format(desc))
  101. else
  102. posix.syslog(3, ("Failed to execute '%s': %s %d"):format(desc, reason, status))
  103. end
  104. return status, table.concat(output)
  105. end
  106. local function configure_bgp(desc, ...)
  107. local args = {
  108. "-d", "bgpd",
  109. "-c", "configure terminal",
  110. }
  111. for _, val in ipairs({...}) do
  112. table.insert(args, "-c")
  113. table.insert(args, val)
  114. end
  115. return execute(desc, "vtysh", table.unpack(args))
  116. end
  117. local last_bgp_reset = 0
  118. local function bgp_reset(msg, local_cert)
  119. local now = os.time()
  120. if last_bgp_reset + 60 > now then return end
  121. last_bgp_reset = now
  122. configure_bgp("spoke reset",
  123. "route-map RTT-SET permit 10", "set metric rtt", "exit",
  124. "route-map RTT-ADD permit 10", "set metric +rtt", "exit",
  125. ("router bgp %d"):format(local_cert.AS),
  126. "no neighbor hubs",
  127. "neighbor hubs peer-group",
  128. "neighbor hubs remote-as 65000",
  129. "neighbor hubs ebgp-multihop 1",
  130. "neighbor hubs disable-connected-check",
  131. "neighbor hubs timers 10 30",
  132. "neighbor hubs timers connect 10",
  133. "neighbor hubs next-hop-self all",
  134. "neighbor hubs soft-reconfiguration inbound",
  135. "neighbor hubs route-map RTT-ADD in")
  136. end
  137. local function bgp_nhs_up(msg, remote_cert, local_cert)
  138. configure_bgp(("nhs-up %s"):format(msg.remote_addr),
  139. ("router bgp %s"):format(local_cert.AS),
  140. ("neighbor %s peer-group hubs"):format(msg.remote_addr))
  141. end
  142. local function bgp_nhs_down(msg, remote_cert, local_cert)
  143. configure_bgp(("nhs-down %s"):format(msg.remote_addr),
  144. ("router bgp %s"):format(local_cert.AS),
  145. ("no neighbor %s"):format(msg.remote_addr))
  146. end
  147. local function bgp_create_spoke_rules(msg, remote_cert, local_cert)
  148. if not local_cert.hub then return end
  149. local bgpcfg = {}
  150. for seq, net in ipairs(remote_cert.NET) do
  151. table.insert(bgpcfg,
  152. ("ip prefix-list net-%s-in seq %d permit %s le %d"):format(
  153. msg.remote_addr, seq * 5, net,
  154. remote_cert.hub and 32 or 26))
  155. end
  156. table.insert(bgpcfg, ("router bgp %s"):format(local_cert.AS))
  157. if remote_cert.hub then
  158. table.insert(bgpcfg, ("neighbor %s peer-group hubs"):format(msg.remote_addr))
  159. elseif local_cert.AS == remote_cert.AS then
  160. table.insert(bgpcfg, ("neighbor %s peer-group spoke-ibgp"):format(msg.remote_addr))
  161. else
  162. table.insert(bgpcfg, ("neighbor %s remote-as %s"):format(msg.remote_addr, remote_cert.AS))
  163. table.insert(bgpcfg, ("neighbor %s peer-group spoke-ebgp"):format(msg.remote_addr))
  164. end
  165. table.insert(bgpcfg, ("neighbor %s prefix-list net-%s-in in"):format(msg.remote_addr, msg.remote_addr))
  166. local status, output = configure_bgp(("nhc-register %s"):format(msg.remote_addr), table.unpack(bgpcfg))
  167. if output:find("Cannot") then
  168. posix.syslog(6, "BGP: "..output)
  169. configure_bgp(
  170. ("nhc-recreate %s"):format(msg.remote_addr),
  171. ("router bgp %s"):format(local_cert.AS),
  172. ("no neighbor %s"):format(msg.remote_addr),
  173. table.unpack(bgpcfg))
  174. end
  175. end
  176. local function handle_message(msg)
  177. if msg.event ~= "authorize-binding" then return end
  178. -- Verify protocol address against certificate
  179. local auth = false
  180. local local_cert = parse_cert(msg.local_cert)
  181. local remote_cert = parse_cert(msg.remote_cert)
  182. for _, gre in pairs(remote_cert.GRE) do
  183. if gre == msg.remote_addr then auth = true end
  184. end
  185. if not auth then
  186. posix.syslog(3, ("GRE %s to NBMA %s DENIED (cert '%s', allows: %s)"):format(
  187. msg.remote_addr, msg.remote_nbma,
  188. remote_cert.cn, table.concat(remote_cert.GRE, " ")))
  189. return "deny"
  190. end
  191. posix.syslog(6, ("GRE %s to NBMA %s authenticated for %s"):format(
  192. msg.remote_addr, msg.remote_nbma, remote_cert.cn))
  193. -- Automatic BGP binding for hub-spoke connections
  194. if msg.type == "nhs" and msg.old_type ~= "nhs" then
  195. if not local_cert.hub then
  196. if tonumber(msg.num_nhs) == 0 and msg.vc_initiated == "yes" then
  197. bgp_reset(msg, local_cert)
  198. end
  199. bgp_nhs_up(msg, remote_cert, local_cert)
  200. else
  201. bgp_create_spoke_rules(msg, remote_cert, local_cert)
  202. end
  203. elseif msg.type ~= "nhs" and msg.old_type == "nhs" then
  204. bgp_nhs_down(msg, remote_cert, local_cert)
  205. elseif msg.type == "dynamic" and msg.old_type ~= "dynamic" then
  206. bgp_create_spoke_rules(msg, remote_cert, local_cert)
  207. end
  208. return "accept"
  209. end
  210. local function handle_connection(conn)
  211. local msg = {}
  212. for l in conn:lines() do
  213. if l == "" then
  214. res = handle_message(msg)
  215. if msg.eventid then
  216. conn:write(("eventid=%s\nresult=%s\n\n"):format(msg.eventid, res or "default"))
  217. end
  218. msg = {}
  219. else
  220. local key, value = l:match('([^=]*)=(.*)')
  221. if key and value then
  222. msg[key] = value
  223. end
  224. end
  225. end
  226. conn:close()
  227. end
  228. loop:wrap(function()
  229. while true do
  230. local conn = listener:accept()
  231. conn:setmode("b", "bl")
  232. loop:wrap(function()
  233. local ok, msg = pcall(handle_connection, conn)
  234. if not ok then posix.syslog(3, msg) end
  235. conn:close()
  236. end)
  237. end
  238. end)
  239. print(loop:loop())