Skip to content

Instantly share code, notes, and snippets.

@palkan
Last active November 2, 2021 15:32
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save palkan/74885c6e3fee6a0c3e63cd606740fd63 to your computer and use it in GitHub Desktop.
Save palkan/74885c6e3fee6a0c3e63cd606740fd63 to your computer and use it in GitHub Desktop.
tcpdump -> pretty http2 packets
# frozen_string_literal: true
# Example usage:
#
# tcpdump -i lo0 'port 3010' -v -X -l | ruby frame_display.rb
#
require 'bundler/inline'
gemfile(true, quiet: true) do
source 'https://rubygems.org'
gem 'http-2'
# gem 'debug' # uncomment for debugging
end
require 'http/2/buffer'
require 'http/2/framer'
def hex_to_array(str)
str.gsub(/\s+/m, '').scan(/../).map { _1.to_i(16).chr }
end
def parse_ipv4(str, **opts)
arr = hex_to_array(str)
payload = arr[52...]
return if payload.nil?
parse_frame(payload.join, **opts)
end
def parse_frame(source, **opts)
buffer = HTTP2::Buffer.new(source)
framer = HTTP2::Framer.new
loop do
frame = framer.parse(buffer)
break unless frame
print_frame(frame, **opts)
end
end
TYPE_TO_COLOR = {
headers: "\e[34m", # blue
data: "\e[32m" # green
}.freeze
DEFAULT_COLOR = "\e[33m" # yellow
def print_frame(frame, direction: nil)
type = frame.delete(:type)
clr = TYPE_TO_COLOR.fetch(type, DEFAULT_COLOR)
annotation = +"Stream: #{frame.delete(:stream)} Length: #{frame.delete(:length)}"
annotation << " (#{direction})" if direction
puts "#{clr}#{type.to_s.upcase}\e[0m [\e[36m#{frame.delete(:flags).map do
_1.to_s.upcase
end.join(' ')}\e[0m] \e[90m# #{annotation}\e[0m\n"
frame.delete(:payload).then do |payload|
next unless payload
puts " #{payload.inspect}"
end
puts " #{frame.inspect}" unless frame.empty?
puts
end
def read_dump(io)
current_packet = nil
direction = nil
expected_length = nil
io.each_line do |line|
case line
# 21:52:25.397055 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 64,
when /^\d{2}:\d{2}:\d{2}\.\d+\sIP.+length (\d+)/
current_packet = []
expected_length = Regexp.last_match[1].to_i * 2
# localhost.gw > localhost.64902: Flags [.], cksum 0xfe28 (incorrect -> 0x158b), ack 110, win
when /^\s+([\w\-.]+ > [\w\-.]+): Flags/
direction = Regexp.last_match[1]
# 0x0000: 4500 0034 0000 4000 4006 0000 7f00 0001 E..4..@.@.......
when /^\s+0x[a-f\d]{4}:\s\s(([a-f\d]{4}\s)*[a-f\d]{2,})\s\s/
current_packet ||= []
bytes = Regexp.last_match[1].split(/\s+/).join.chars
current_packet.concat bytes
parse_ipv4(current_packet.join, direction: direction) if current_packet.size == expected_length
end
end
end
# Pipe tcdump
if $stdin.stat.pipe?
read_dump($stdin)
elsif ARGV[0]
read_dump(ARGF)
else
raise 'Please, provide path to frame dump file as the first argument or pipe tcpdump to the script'
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment