mirror of
				https://git.haproxy.org/git/haproxy.git/
				synced 2025-10-31 16:41:01 +01:00 
			
		
		
		
	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.
		
			
				
	
	
		
			248 lines
		
	
	
		
			7.8 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
			
		
		
	
	
			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)
 |