haproxy/dev/h2/h2-tracer.lua
Aurelien DARRAGON afd5f5d671 DEV: h2: fix h2-tracer.lua nil value index
Nick Ramirez reported the following error while testing the h2-tracer.lua
script:

  Lua filter 'h2-tracer' : [state-id 0] runtime error: /etc/haproxy/h2-tracer.lua:227: attempt to index a nil value (field '?') from /etc/haproxy/h2-tracer.lua:227: in function line 109.

It is caused by h2ff indexing with an out of bound value. Indeed, h2ff
is indexed with the frame type, which can potentially be > 9 (not common
nor observed during Willy's tests), while h2ff only defines indexes from
0 to 9.

The fix was provided by Willy, it consists in skipping h2ff indexing if
frame type is > 9. It was confirmed that doing so fixes the error.
2025-04-08 17:44:41 +02:00

248 lines
7.8 KiB
Lua

-- This is an HTTP/2 tracer for a TCP proxy. It will decode the frames that are
-- exchanged between the client and the server and indicate their direction,
-- types, flags and lengths. Lines are prefixed with a connection number modulo
-- 4096 that allows to sort out multiplexed exchanges. In order to use this,
-- simply load this file in the global section and use it from a TCP proxy:
--
-- global
-- lua-load "dev/h2/h2-tracer.lua"
--
-- listen h2_sniffer
-- mode tcp
-- bind :8002
-- filter lua.h2-tracer #hex
-- server s1 127.0.0.1:8003
--
-- define the decoder's class here
Dec = {}
Dec.id = "Lua H2 tracer"
Dec.flags = 0
Dec.__index = Dec
Dec.args = {} -- args passed by the filter's declaration
Dec.cid = 0 -- next connection ID
-- prefix to indent responses
res_pfx = " | "
-- H2 frame types
h2ft = {
[0] = "DATA",
[1] = "HEADERS",
[2] = "PRIORITY",
[3] = "RST_STREAM",
[4] = "SETTINGS",
[5] = "PUSH_PROMISE",
[6] = "PING",
[7] = "GOAWAY",
[8] = "WINDOW_UPDATE",
[9] = "CONTINUATION",
}
h2ff = {
[0] = { [0] = "ES", [3] = "PADDED" }, -- data
[1] = { [0] = "ES", [2] = "EH", [3] = "PADDED", [5] = "PRIORITY" }, -- headers
[2] = { }, -- priority
[3] = { }, -- rst_stream
[4] = { [0] = "ACK" }, -- settings
[5] = { [2] = "EH", [3] = "PADDED" }, -- push_promise
[6] = { [0] = "ACK" }, -- ping
[7] = { }, -- goaway
[8] = { }, -- window_update
[9] = { [2] = "EH" }, -- continuation
}
function Dec:new()
local dec = {}
setmetatable(dec, Dec)
dec.do_hex = false
if (Dec.args[1] == "hex") then
dec.do_hex = true
end
Dec.cid = Dec.cid+1
-- mix the thread number when multithreading.
dec.cid = Dec.cid + 64 * core.thread
-- state per dir. [1]=req [2]=res
dec.st = {
[1] = {
hdr = { 0, 0, 0, 0, 0, 0, 0, 0, 0 },
fofs = 0,
flen = 0,
ftyp = 0,
fflg = 0,
sid = 0,
tot = 0,
},
[2] = {
hdr = { 0, 0, 0, 0, 0, 0, 0, 0, 0 },
fofs = 0,
flen = 0,
ftyp = 0,
fflg = 0,
sid = 0,
tot = 0,
},
}
return dec
end
function Dec:start_analyze(txn, chn)
if chn:is_resp() then
io.write(string.format("[%03x] ", self.cid % 4096) .. res_pfx .. "### res start\n")
else
io.write(string.format("[%03x] ", self.cid % 4096) .. "### req start\n")
end
filter.register_data_filter(self, chn)
end
function Dec:end_analyze(txn, chn)
if chn:is_resp() then
io.write(string.format("[%03x] ", self.cid % 4096) .. res_pfx .. "### res end: " .. self.st[2].tot .. " bytes total\n")
else
io.write(string.format("[%03x] ", self.cid % 4096) .. "### req end: " ..self.st[1].tot.. " bytes total\n")
end
end
function Dec:tcp_payload(txn, chn)
local data = { }
local dofs = 1
local pfx = ""
local dir = 1
local sofs = 0
local ft = ""
local ff = ""
if chn:is_resp() then
pfx = res_pfx
dir = 2
end
pfx = string.format("[%03x] ", self.cid % 4096) .. pfx
-- stream offset before processing
sofs = self.st[dir].tot
if (chn:input() > 0) then
data = chn:data()
self.st[dir].tot = self.st[dir].tot + chn:input()
end
if (chn:input() > 0 and self.do_hex ~= false) then
io.write("\n" .. pfx .. "Hex:\n")
for i = 1, #data do
if ((i & 7) == 1) then io.write(pfx) end
io.write(string.format("0x%02x ", data:sub(i, i):byte()))
if ((i & 7) == 0 or i == #data) then io.write("\n") end
end
end
-- start at byte 1 in the <data> string
dofs = 1
-- the first 24 bytes are expected to be an H2 preface on the request
if (dir == 1 and sofs < 24) then
-- let's not check it for now
local bytes = self.st[dir].tot - sofs
if (sofs + self.st[dir].tot >= 24) then
-- skip what was missing from the preface
dofs = dofs + 24 - sofs
sofs = 24
io.write(pfx .. "[PREFACE len=24]\n")
else
-- consume more preface bytes
sofs = sofs + self.st[dir].tot
return
end
end
-- parse contents as long as there are pending data
while true do
-- check if we need to consume data from the current frame
-- flen is the number of bytes left before the frame's end.
if (self.st[dir].flen > 0) then
if dofs > #data then return end -- missing data
if (#data - dofs + 1 < self.st[dir].flen) then
-- insufficient data
self.st[dir].flen = self.st[dir].flen - (#data - dofs + 1)
io.write(pfx .. string.format("%32s\n", "... -" .. (#data - dofs + 1) .. " = " .. self.st[dir].flen))
dofs = #data + 1
return
else
-- enough data to finish
if (dofs == 1) then
-- only print a partial size if the frame was interrupted
io.write(pfx .. string.format("%32s\n", "... -" .. self.st[dir].flen .. " = 0"))
end
dofs = dofs + self.st[dir].flen
self.st[dir].flen = 0
end
end
-- here, flen = 0, we're at the beginning of a new frame --
-- read possibly missing header bytes until dec.fofs == 9
while self.st[dir].fofs < 9 do
if dofs > #data then return end -- missing data
self.st[dir].hdr[self.st[dir].fofs + 1] = data:sub(dofs, dofs):byte()
dofs = dofs + 1
self.st[dir].fofs = self.st[dir].fofs + 1
end
-- we have a full frame header here
if (self.do_hex ~= false) then
io.write("\n" .. pfx .. string.format("hdr=%02x %02x %02x %02x %02x %02x %02x %02x %02x\n",
self.st[dir].hdr[1], self.st[dir].hdr[2], self.st[dir].hdr[3],
self.st[dir].hdr[4], self.st[dir].hdr[5], self.st[dir].hdr[6],
self.st[dir].hdr[7], self.st[dir].hdr[8], self.st[dir].hdr[9]))
end
-- we have a full frame header, we'll be ready
-- for a new frame once the data is gone
self.st[dir].flen = self.st[dir].hdr[1] * 65536 +
self.st[dir].hdr[2] * 256 +
self.st[dir].hdr[3]
self.st[dir].ftyp = self.st[dir].hdr[4]
self.st[dir].fflg = self.st[dir].hdr[5]
self.st[dir].sid = self.st[dir].hdr[6] * 16777216 +
self.st[dir].hdr[7] * 65536 +
self.st[dir].hdr[8] * 256 +
self.st[dir].hdr[9]
self.st[dir].fofs = 0
-- decode frame type
if self.st[dir].ftyp <= 9 then
ft = h2ft[self.st[dir].ftyp]
else
ft = string.format("TYPE_0x%02x\n", self.st[dir].ftyp)
end
-- decode frame flags for frame type <ftyp>
ff = ""
for i = 7, 0, -1 do
if (((self.st[dir].fflg >> i) & 1) ~= 0) then
if self.st[dir].ftyp <= 9 and h2ff[self.st[dir].ftyp][i] ~= nil then
ff = ff .. ((ff == "") and "" or "+")
ff = ff .. h2ff[self.st[dir].ftyp][i]
else
ff = ff .. ((ff == "") and "" or "+")
ff = ff .. string.format("0x%02x", 1<<i)
end
end
end
io.write(pfx .. string.format("[%s %ssid=%u len=%u (bytes=%u)]\n",
ft, (ff == "") and "" or ff .. " ",
self.st[dir].sid, self.st[dir].flen,
(#data - dofs + 1)))
end
end
core.register_filter("h2-tracer", Dec, function(dec, args)
Dec.args = args
return dec
end)