Import Upstream version 1.2.2
[quagga-debian.git] / nhrpd / nhrp-events.lua
1 #!/usr/bin/lua5.2
2
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.
6
7 -- Depends on lua5.2 lua5.2-posix lua5.2-cqueues lua5.2-ossl lua-asn1
8
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'
16
17 local SOCK = "/var/run/nhrp-events.sock"
18 posix.unlink(SOCK)
19
20 local loop = cq.new()
21 local nulfd = posix.open("/dev/null", posix.O_RDWR)
22 local listener = cqs.listen{path=SOCK}
23
24 posix.chown(SOCK, "quagga", "quagga")
25 posix.setpid("u", "quagga")
26 posix.setpid("g", "quagga")
27 posix.openlog("nhrp-events", "np")
28
29 function string.hex2bin(str)
30         return str:gsub('..', function(cc) return string.char(tonumber(cc, 16)) end)
31 end
32
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())
37 end
38
39 local function do_parse_cert(cert, out)
40         for type, value in pairs(cert:getSubjectAlt()) do
41                 if type == 'IP' then
42                         table.insert(out.GRE, value)
43                 end
44         end
45         if #out.GRE == 0 then return end
46
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
50                         if as.id then
51                                 out.AS = tonumber(as.id)
52                                 break
53                         end
54                 end
55         end
56
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)
63                                 end
64                         end
65                 end
66         end
67
68         return true
69 end
70
71 local function parse_cert(certhex)
72         local out = {
73                 cn = "(no CN)",
74                 AS = 0,
75                 GRE = {},
76                 NET = {},
77         }
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)
83         return out
84 end
85
86 local function execute(desc, cmd, ...)
87         local piper, pipew = posix.pipe()
88         if piper == nil then
89                 return error("Pipe failed")
90         end
91
92         local pid = posix.fork()
93         if pid == -1 then
94                 return error("Fork failed")
95         end
96         if pid == 0 then
97                 posix.close(piper)
98                 posix.dup2(nulfd, 0)
99                 posix.dup2(pipew, 1)
100                 posix.dup2(nulfd, 2)
101                 posix.execp(cmd, ...)
102                 os.exit(1)
103         end
104         posix.close(pipew)
105
106         -- This blocks -- perhaps should handle command executions in separate queue.
107         local output = {}
108         while true do
109                 local d = posix.read(piper, 8192)
110                 if d == nil or d == "" then break end
111                 table.insert(output, d)
112         end
113         posix.close(piper)
114
115         local _, reason, status = posix.wait(pid)
116         if status == 0 then
117                 posix.syslog(6, ("Executed '%s' successfully"):format(desc))
118         else
119                 posix.syslog(3, ("Failed to execute '%s': %s %d"):format(desc, reason, status))
120         end
121         return status, table.concat(output)
122 end
123
124 local function configure_bgp(desc, ...)
125         local args = {
126                 "-d", "bgpd",
127                 "-c", "configure terminal",
128         }
129         for _, val in ipairs({...}) do
130                 table.insert(args, "-c")
131                 table.insert(args, val)
132         end
133         return execute(desc, "vtysh", table.unpack(args))
134 end
135
136 local last_bgp_reset = 0
137
138 local function bgp_reset(msg, local_cert)
139         local now = os.time()
140         if last_bgp_reset + 60 > now then return end
141         last_bgp_reset = now
142
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),
147                 "no neighbor hubs",
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")
157 end
158
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))
163 end
164
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))
169 end
170
171 local function bgp_create_spoke_rules(msg, remote_cert, local_cert)
172         if not local_cert.hub then return end
173
174         local bgpcfg = {}
175         for seq, net in ipairs(remote_cert.NET) do
176                 table.insert(bgpcfg,
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))
180         end
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))
186         else
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))
189         end
190         table.insert(bgpcfg, ("neighbor %s prefix-list net-%s-in in"):format(msg.remote_addr, msg.remote_addr))
191
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)
195                 configure_bgp(
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))
200         end
201 end
202
203 local function handle_message(msg)
204         if msg.event ~= "authorize-binding" then return end
205
206         -- Verify protocol address against certificate
207         local auth = false
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
212         end
213         if not auth then
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, " ")))
217                 return "deny"
218         end
219         posix.syslog(6, ("GRE %s to NBMA %s authenticated for %s"):format(
220                 msg.remote_addr, msg.remote_nbma, remote_cert.cn))
221
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)
227                         end
228                         bgp_nhs_up(msg, remote_cert, local_cert)
229                 else
230                         bgp_create_spoke_rules(msg, remote_cert, local_cert)
231                 end
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)
236         end
237
238         return "accept"
239 end
240
241 local function handle_connection(conn)
242         local msg = {}
243         for l in conn:lines() do
244                 if l == "" then
245                         res = handle_message(msg)
246                         if msg.eventid then
247                                 conn:write(("eventid=%s\nresult=%s\n\n"):format(msg.eventid, res or "default"))
248                         end
249                         msg = {}
250                 else
251                         local key, value = l:match('([^=]*)=(.*)')
252                         if key and value then
253                                 msg[key] = value
254                         end
255                 end
256         end
257         conn:close()
258 end
259
260 loop:wrap(function()
261         while true do
262                 local conn = listener:accept()
263                 conn:setmode("b", "bl")
264                 loop:wrap(function()
265                         local ok, msg = pcall(handle_connection, conn)
266                         if not ok then posix.syslog(3, msg) end
267                         conn:close()
268                 end)
269         end
270 end)
271
272 print(loop:loop())