Class: PacketFu::Packet
- Inherits:
-
Object
- Object
- PacketFu::Packet
- Defined in:
- lib/packetfu/packet.rb
Overview
Packet is the parent class of EthPacket, IPPacket, UDPPacket, TCPPacket, and all other packets. It acts as both a singleton class, so things like Packet.parse can happen, and as an abstract class to provide subclasses some structure.
Direct Known Subclasses
ARPPacket, EthPacket, HSRPPacket, ICMPPacket, ICMPv6Packet, IPPacket, IPv6Packet, InvalidPacket, LLDPPacket, TCPPacket, UDPPacket
Instance Attribute Summary collapse
-
#flavor ⇒ Object
readonly
Packet Headers are responsible for their own specific flavor methods.
-
#headers ⇒ Object
All packets have a header collection, useful for determining protocol trees.
-
#iface ⇒ Object
Default inferface to send packets to.
-
#inspect_style ⇒ Object
Default is :dissect, can also be :hex or :default.
Class Method Summary collapse
-
.can_parse?(str) ⇒ Boolean
Packet subclasses must override this, since the Packet superclass can’t actually parse anything.
-
.force_binary(str) ⇒ Object
Force strings into binary.
-
.inherited(subclass) ⇒ Object
Register subclasses in PacketFu.packet_class to do all kinds of neat things that obviates those long if/else trees for parsing.
-
.layer ⇒ Object
Defines the layer this packet type lives at, based on the number of headers it requires.
- .layer_symbol ⇒ Object
-
.parse(packet = nil, args = {}) ⇒ Object
Parse() creates the correct packet type based on the data, and returns the apporpiate Packet subclass object.
Instance Method Summary collapse
-
#==(other) ⇒ Object
If two packets are represented as the same binary string, and they’re both actually PacketFu packets of the same sort, they’re equal.
-
#clone ⇒ Object
Packets are bundles of lots of objects, so copying them is a little complicated – a dup of a packet is actually full of pass-by-reference stuff in the @headers, so if you change one, you’re changing all this copies, too.
-
#dissect ⇒ Object
Renders the dissection_table suitable for screen printing.
- #dissection_table ⇒ Object
- #handle_is_identity(ptype) ⇒ Object
-
#hexify(str) ⇒ Object
Hexify provides a neatly-formatted dump of binary data, familar to hex readers.
-
#initialize(args = {}) ⇒ Packet
constructor
the Packet class should not be instantiated directly, since it’s an abstract class that real packet types inherit from.
-
#inspect ⇒ Object
For packets, inspect is overloaded as inspect_hex(0).
-
#inspect_hex(arg = 0) ⇒ Object
If @inspect_style is :default (or :ugly), the inspect output is the usual inspect.
- #kind_of?(klass) ⇒ Boolean
- #layer ⇒ Object
- #layer_symbol ⇒ Object
-
#method_missing(sym, *args, &block) ⇒ Object
method_missing() delegates protocol-specific field actions to the apporpraite class variable (which contains the associated packet type) This register-of-protocols style switch will work for the forseeable future (there aren’t /that/ many packet types), and it’s a handy way to know at a glance what packet types are supported.
- #orig_kind_of? ⇒ Object
-
#payload ⇒ Object
Get the outermost payload (body) of the packet; this is why all packet headers should have a body type.
-
#payload=(args) ⇒ Object
Set the outermost payload (body) of the packet.
-
#peek(args = {}) ⇒ Object
Peek provides summary data on packet contents.
-
#peek_format ⇒ Object
The peek_format is used to display a single line of packet data useful for eyeballing.
-
#proto ⇒ Object
(also: #protocol)
Returns an array of protocols contained in this packet.
-
#read(str = nil, args = {}) ⇒ Object
Read() takes (and trusts) the io input and shoves it all into a well-formed Packet.
-
#recalc(arg = :all) ⇒ Object
Recalculates all the calcuated fields for all headers in the packet.
- #respond_to?(sym, include_private = false) ⇒ Boolean
-
#size ⇒ Object
(also: #length)
Returns the size of the packet (as a binary string).
-
#to_f(filename = nil, mode = 'w') ⇒ Object
Put the entire packet into a libpcap file.
-
#to_pcap(args = {}) ⇒ Object
Converts a packet to libpcap format.
-
#to_s ⇒ Object
Get the binary string of the entire packet.
-
#to_w(iface = nil) ⇒ Object
Put the entire packet on the wire by creating a temporary PacketFu::Inject object.
-
#write(io) ⇒ Object
In the event of no proper decoding, at least send it to the inner-most header.
Constructor Details
#initialize(args = {}) ⇒ Packet
the Packet class should not be instantiated directly, since it’s an abstract class that real packet types inherit from. Sadly, this makes the Packet class more difficult to test directly.
477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 |
# File 'lib/packetfu/packet.rb', line 477 def initialize(args={}) if self.class.name =~ /(::|^)PacketFu::Packet$/ raise NoMethodError, "method `new' called for abstract class #{self.class.name}" end @inspect_style = args[:inspect_style] || PacketFu.inspect_style || :dissect if args[:config] args[:config].each_pair do |k,v| case k when :eth_daddr; @eth_header.eth_daddr=v if @eth_header when :eth_saddr; @eth_header.eth_saddr=v if @eth_header when :ip_saddr; @ip_header.ip_saddr=v if @ip_header when :iface; @iface = v end end end end |
Dynamic Method Handling
This class handles dynamic methods through the method_missing method
#method_missing(sym, *args, &block) ⇒ Object
method_missing() delegates protocol-specific field actions to the apporpraite class variable (which contains the associated packet type) This register-of-protocols style switch will work for the forseeable future (there aren’t /that/ many packet types), and it’s a handy way to know at a glance what packet types are supported.
506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 |
# File 'lib/packetfu/packet.rb', line 506 def method_missing(sym, *args, &block) case sym.to_s when /^is_([a-zA-Z0-9]+)\?/ ptype = $1 if PacketFu.packet_prefixes.index(ptype) self.send(:handle_is_identity, $1) else super end when /^([a-zA-Z0-9]+)_.+/ ptype = $1 if PacketFu.packet_prefixes.index(ptype) self.instance_variable_get("@#{ptype}_header").send(sym,*args, &block) else super end else super end end |
Instance Attribute Details
#flavor ⇒ Object (readonly)
Packet Headers are responsible for their own specific flavor methods.
11 12 13 |
# File 'lib/packetfu/packet.rb', line 11 def flavor @flavor end |
#headers ⇒ Object
All packets have a header collection, useful for determining protocol trees.
12 13 14 |
# File 'lib/packetfu/packet.rb', line 12 def headers @headers end |
#iface ⇒ Object
Default inferface to send packets to
13 14 15 |
# File 'lib/packetfu/packet.rb', line 13 def iface @iface end |
#inspect_style ⇒ Object
Default is :dissect, can also be :hex or :default
14 15 16 |
# File 'lib/packetfu/packet.rb', line 14 def inspect_style @inspect_style end |
Class Method Details
.can_parse?(str) ⇒ Boolean
Packet subclasses must override this, since the Packet superclass can’t actually parse anything.
292 293 294 |
# File 'lib/packetfu/packet.rb', line 292 def self.can_parse?(str) false end |
.force_binary(str) ⇒ Object
Force strings into binary.
23 24 25 |
# File 'lib/packetfu/packet.rb', line 23 def self.force_binary(str) str.force_encoding Encoding::BINARY if str.respond_to? :force_encoding end |
.inherited(subclass) ⇒ Object
Register subclasses in PacketFu.packet_class to do all kinds of neat things that obviates those long if/else trees for parsing. It’s pretty sweet.
18 19 20 |
# File 'lib/packetfu/packet.rb', line 18 def self.inherited(subclass) PacketFu.add_packet_class(subclass) end |
.layer ⇒ Object
Defines the layer this packet type lives at, based on the number of headers it requires. Note that this has little to do with the OSI model, since TCP/IP doesn’t really have Session and Presentation layers.
Ethernet and the like are layer 1, IP, IPv6, and ARP are layer 2, TCP, UDP, and other transport protocols are layer 3, and application protocols are at layer 4 or higher. InvalidPackets have an arbitrary layer 0 to distinguish them.
Because these don’t change much, it’s cheaper just to case through them, and only resort to counting headers if we don’t have a match – this makes adding protocols somewhat easier, but of course you can just override this method over there, too. This is merely optimized for the most likely protocols you see on the Internet.
261 262 263 264 265 266 267 268 269 270 |
# File 'lib/packetfu/packet.rb', line 261 def self.layer case self.name # Lol ran into case's fancy treatment of classes when /InvalidPacket$/; 0 when /EthPacket$/; 1 when /IPPacket$/, /ARPPacket$/, /LLDPPacket$/, /IPv6Packet$/; 2 when /TCPPacket$/, /UDPPacket$/, /ICMPPacket$/; 3 when /HSRPPacket$/; 4 else; self.new.headers.size end end |
.layer_symbol ⇒ Object
276 277 278 279 280 281 282 283 284 |
# File 'lib/packetfu/packet.rb', line 276 def self.layer_symbol case self.layer when 0; :invalid when 1; :link when 2; :internet when 3; :transport else; :application end end |
.parse(packet = nil, args = {}) ⇒ Object
Parse() creates the correct packet type based on the data, and returns the apporpiate Packet subclass object.
There is an assumption here that all incoming packets are either EthPacket or InvalidPacket types. This will be addressed pretty soon.
If application-layer parsing is /not/ desired, that should be indicated explicitly with an argument of :parse_app => false. Otherwise, app-layer parsing will happen.
It is no longer neccisary to manually add packet types here.
37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
# File 'lib/packetfu/packet.rb', line 37 def self.parse(packet=nil,args={}) parse_app = true if(args[:parse_app].nil? or args[:parse_app]) force_binary(packet) if parse_app classes = PacketFu.packet_classes_by_layer else classes = PacketFu.packet_classes_by_layer_without_application end new_args = {} new_args[:on_ipv6] = true if PacketFu::IPv6Packet.can_parse?(packet) p = classes.detect { |pclass| pclass.can_parse?(packet) }.new(new_args) parsed_packet = p.read(packet,args) end |
Instance Method Details
#==(other) ⇒ Object
If two packets are represented as the same binary string, and they’re both actually PacketFu packets of the same sort, they’re equal.
The intuitive result is that a packet of a higher layer (like DNSPacket) can be equal to a packet of a lower level (like UDPPacket) as long as the bytes are equal (this can come up if a transport-layer packet has a hand-crafted payload that is identical to what would have been created by using an application layer packet)
199 200 201 202 203 |
# File 'lib/packetfu/packet.rb', line 199 def ==(other) return false unless other.kind_of? self.class return false unless other.respond_to? :to_s self.to_s == other.to_s end |
#clone ⇒ Object
Packets are bundles of lots of objects, so copying them is a little complicated – a dup of a packet is actually full of pass-by-reference stuff in the @headers, so if you change one, you’re changing all this copies, too.
Normally, this doesn’t seem to be a big deal, and it’s a pretty decent performance tradeoff. But, if you’re going to be creating a template packet to base a bunch of slightly different ones off of (like a fuzzer might), you’ll want to use clone()
187 188 189 |
# File 'lib/packetfu/packet.rb', line 187 def clone Packet.parse(self.to_s) end |
#dissect ⇒ Object
Renders the dissection_table suitable for screen printing. Can take one or two arguments. If just the one, only that layer will be displayed take either a range or a number – if a range, only protos within that range will be rendered. If an integer, only that proto will be rendered.
370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 |
# File 'lib/packetfu/packet.rb', line 370 def dissect dtable = self.dissection_table hex_body = nil if dtable.last.kind_of?(Array) and dtable.last.first == :body body = dtable.pop hex_body = hexify(body[1]) end elem_widths = [0,0,0] dtable.each do |proto_table| proto_table[1].each do |elems| elems.each_with_index do |e,i| width = e.size elem_widths[i] = width if width > elem_widths[i] end end end total_width = elem_widths.inject(0) {|sum,x| sum+x} table = "" dtable.each do |proto| table << "--" table << proto[0] if total_width > proto[0].size table << ("-" * (total_width - proto[0].size + 2)) else table << ("-" * (total_width + 2)) end table << "\n" proto[1].each do |elems| table << " " elems_table = [] (0..2).each do |i| elems_table << ("%-#{elem_widths[i]}s" % elems[i]) end table << elems_table.join("\s") table << "\n" end end if hex_body && !hex_body.empty? table << "-" * 66 table << "\n" table << "00-01-02-03-04-05-06-07-08-09-0a-0b-0c-0d-0e-0f---0123456789abcdef\n" table << "-" * 66 table << "\n" table << hex_body end table end |
#dissection_table ⇒ Object
338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 |
# File 'lib/packetfu/packet.rb', line 338 def dissection_table table = [] @headers.each_with_index do |header,table_idx| proto = header.class.name.sub(/^.*::/,"") table << [proto,[]] header.class.members.each do |elem| elem_sym = elem.to_sym # to_sym needed for 1.8 next if elem_sym == :body elem_type_value = [] elem_type_value[0] = elem readable_element = "#{elem}_readable" if header.respond_to? readable_element elem_type_value[1] = header.send(readable_element) else elem_type_value[1] = header.send(elem) end elem_type_value[2] = header[elem.to_sym].class.name table[table_idx][1] << elem_type_value end end table if @headers.last.members.map {|x| x.to_sym }.include? :body body_part = [:body, self.payload, @headers.last.body.class.name] end table << body_part end |
#handle_is_identity(ptype) ⇒ Object
52 53 54 55 56 57 58 59 |
# File 'lib/packetfu/packet.rb', line 52 def handle_is_identity(ptype) idx = PacketFu.packet_prefixes.index(ptype.to_s.downcase) if idx self.kind_of? PacketFu.packet_classes[idx] else raise NoMethodError, "Undefined method `is_#{ptype}?' for #{self.class}." end end |
#hexify(str) ⇒ Object
Hexify provides a neatly-formatted dump of binary data, familar to hex readers.
297 298 299 300 301 302 303 304 305 306 |
# File 'lib/packetfu/packet.rb', line 297 def hexify(str) str.force_encoding(Encoding::BINARY) if str.respond_to? :force_encoding hexascii_lines = str.to_s.unpack("H*")[0].scan(/.{1,32}/) regex = Regexp.new('[\x00-\x1f\x7f-\xff]'.force_encoding('ASCII-8BIT'), Regexp::NOENCODING) chars = str.to_s.gsub(regex,'.') chars_lines = chars.scan(/.{1,16}/) ret = [] hexascii_lines.size.times {|i| ret << "%-48s %s" % [hexascii_lines[i].gsub(/(.{2})/,"\\1 "),chars_lines[i]]} ret.join("\n") end |
#inspect ⇒ Object
For packets, inspect is overloaded as inspect_hex(0). Not sure if this is a great idea yet, but it sure makes the irb output more sane.
If you hate this, you can run PacketFu.toggle_inspect to return to the typical (and often unreadable) Object#inspect format.
439 440 441 442 443 444 445 446 447 448 |
# File 'lib/packetfu/packet.rb', line 439 def inspect case @inspect_style when :dissect self.dissect when :hex self.proto.join("|") + "\n" + self.inspect_hex else super end end |
#inspect_hex(arg = 0) ⇒ Object
If @inspect_style is :default (or :ugly), the inspect output is the usual inspect.
If @inspect_style is :hex (or :pretty), the inspect output is a much more compact hexdump-style, with a shortened set of packet header names at the top.
If @inspect_style is :dissect (or :verbose), the inspect output is the longer, but more readable, dissection of the packet. This is the default.
TODO: Have an option for colors. Everyone loves colorized irb output.
319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 |
# File 'lib/packetfu/packet.rb', line 319 def inspect_hex(arg=0) case arg when :layers ret = [] @headers.size.times do |i| ret << hexify(@headers[i]) end ret when (0..9) if @headers[arg] hexify(@headers[arg]) else nil end when :all inspect_hex(0) end end |
#kind_of?(klass) ⇒ Boolean
420 421 422 423 424 425 426 427 428 429 430 431 |
# File 'lib/packetfu/packet.rb', line 420 def kind_of?(klass) return true if orig_kind_of? klass packet_types = proto.map {|p| PacketFu.const_get("#{p}Packet")} match = false packet_types.each do |p| if p.ancestors.include? klass match = true break end end return match end |
#layer ⇒ Object
272 273 274 |
# File 'lib/packetfu/packet.rb', line 272 def layer self.class.layer end |
#layer_symbol ⇒ Object
286 287 288 |
# File 'lib/packetfu/packet.rb', line 286 def layer_symbol self.class.layer_symbol end |
#orig_kind_of? ⇒ Object
418 |
# File 'lib/packetfu/packet.rb', line 418 alias :orig_kind_of? :kind_of? |
#payload ⇒ Object
Get the outermost payload (body) of the packet; this is why all packet headers should have a body type.
73 74 75 |
# File 'lib/packetfu/packet.rb', line 73 def payload @headers.last.body end |
#payload=(args) ⇒ Object
Set the outermost payload (body) of the packet.
78 79 80 |
# File 'lib/packetfu/packet.rb', line 78 def payload=(args) @headers.last.body=(args) end |
#peek(args = {}) ⇒ Object
Peek provides summary data on packet contents.
Each packet type should provide a peek_format.
208 209 210 211 212 213 214 215 |
# File 'lib/packetfu/packet.rb', line 208 def peek(args={}) idx = @headers.reverse.map {|h| h.respond_to? peek_format}.index(true) if idx @headers.reverse[idx].peek_format else peek_format end end |
#peek_format ⇒ Object
The peek_format is used to display a single line of packet data useful for eyeballing. It should not exceed 80 characters. The Packet superclass defines an example peek_format, but it should hardly ever be triggered, since peek traverses the @header list in reverse to find a suitable format.
Format
* A one or two character protocol initial. It should be unique
* The packet size
* Useful data in a human-usable form.
Ideally, related peek_formats will all line up with each other when printed to the screen.
Example
tcp_packet.peek
#=> "T 1054 10.10.10.105:55000 -> 192.168.145.105:80 [......] S:adc7155b|I:8dd0"
tcp_packet.peek.size
#=> 79
240 241 242 243 244 245 |
# File 'lib/packetfu/packet.rb', line 240 def peek_format peek_data = ["? "] peek_data << "%-5d" % self.to_s.size peek_data << "%68s" % self.to_s[0,34].unpack("H*")[0] peek_data.join end |
#proto ⇒ Object Also known as: protocol
Returns an array of protocols contained in this packet. For example:
t = PacketFu::TCPPacket.new
=> 00 1a c5 00 00 00 00 1a c5 00 00 00 08 00 45 00 ..............E.
00 28 3c ab 00 00 ff 06 7f 25 00 00 00 00 00 00 .(<......%......
00 00 93 5e 00 00 ad 4f e4 a4 00 00 00 00 50 00 ...^...O......P.
40 00 4a 92 00 00 @.J...
t.proto
=> ["Eth", "IP", "TCP"]
465 466 467 468 469 |
# File 'lib/packetfu/packet.rb', line 465 def proto type_array = [] self.headers.each {|header| type_array << header.class.to_s.split('::').last.gsub(/Header$/,'')} type_array end |
#read(str = nil, args = {}) ⇒ Object
Read() takes (and trusts) the io input and shoves it all into a well-formed Packet. Note that read is a destructive process, so any existing data will be lost.
A note on the :strip => true argument: If :strip is set, defined lengths of data will be believed, and any trailers (such as frame check sequences) will be chopped off. This helps to ensure well-formed packets, at the cost of losing perhaps important FCS data.
If :strip is false, header lengths are /not/ believed, and all data will be piped in. When capturing from the wire, this is usually fine, but recalculating the length before saving or re-transmitting will absolutely change the data payload; FCS data will become part of the TCP data as far as tcp_len is concerned. Some effort has been made to preserve the “real” payload for the purposes of checksums, but currently, it’s impossible to seperate new payload data from old trailers, so things like pkt.payload += “some data” will not work correctly.
So, to summarize; if you intend to alter the data, use :strip. If you don’t, don’t. Also, this is a horrid hack. Stripping is useful (and fun!), but the default behavior really should be to create payloads correctly, and /not/ treat extra FCS data as a payload.
Finally, packet subclasses should take two arguments: the string that is the data to be transmuted into a packet, as well as args. This superclass method is merely concerned with handling args common to many packet formats (namely, fixing packets on the fly)
166 167 168 169 170 171 172 173 174 175 |
# File 'lib/packetfu/packet.rb', line 166 def read(str=nil, args={}) raise "Cannot parse `#{str}'" unless self.class.can_parse?(str) @eth_header.read(str) if args[:fix] || args[:recalc] ip_recalc(:ip_sum) if self.is_ip? recalc(:tcp) if self.is_tcp? recalc(:udp) if self.is_udp? end self end |
#recalc(arg = :all) ⇒ Object
Recalculates all the calcuated fields for all headers in the packet. This is important since read() wipes out all the calculated fields such as length and checksum and what all.
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
# File 'lib/packetfu/packet.rb', line 119 def recalc(arg=:all) case arg when :ip ip_recalc(:all) when :ipv6 ipv6_recalc(:all) when :icmp icmp_recalc(:all) when :udp udp_recalc(:all) when :tcp tcp_recalc(:all) when :all ip_recalc(:all) if @ip_header ipv6_recalc(:all) if @ipv6_header icmp_recalc(:all) if @icmp_header udp_recalc(:all) if @udp_header tcp_recalc(:all) if @tcp_header else raise ArgumentError, "Recalculating `#{arg}' unsupported. Try :all" end @headers[0] end |
#respond_to?(sym, include_private = false) ⇒ Boolean
527 528 529 530 531 532 533 534 535 536 537 538 539 |
# File 'lib/packetfu/packet.rb', line 527 def respond_to?(sym, include_private = false) if sym.to_s =~ /^(invalid|eth|arp|ip|icmp|udp|hsrp|tcp|ipv6)_/ self.instance_variable_get("@#{$1}_header").respond_to? sym elsif sym.to_s =~ /^is_([a-zA-Z0-9]+)\?/ if PacketFu.packet_prefixes.index($1) true else super end else super end end |
#size ⇒ Object Also known as: length
Returns the size of the packet (as a binary string)
451 452 453 |
# File 'lib/packetfu/packet.rb', line 451 def size self.to_s.size end |
#to_f(filename = nil, mode = 'w') ⇒ Object
Put the entire packet into a libpcap file. XXX: this is a hack for now just to confirm that packets are getting created correctly. Now with append! XXX: Document this!
94 95 96 97 98 99 100 101 102 103 104 105 |
# File 'lib/packetfu/packet.rb', line 94 def to_f(filename=nil,mode='w') filename ||= 'out.pcap' mode = mode.to_s[0,1] + "b" raise ArgumentError, "Unknown mode: #{mode.to_s}" unless mode =~ /^[wa]/ if(mode == 'w' || !(File.exist?(filename))) data = [PcapHeader.new, self.to_pcap].map {|x| x.to_s}.join else data = self.to_pcap end File.open(filename, mode) {|f| f.write data} return [filename, 1, data.size] end |
#to_pcap(args = {}) ⇒ Object
Converts a packet to libpcap format. Bit of a hack?
83 84 85 86 87 88 89 |
# File 'lib/packetfu/packet.rb', line 83 def to_pcap(args={}) p = PcapPacket.new(:endian => args[:endian], :timestamp => Timestamp.new.to_s, :incl_len => self.to_s.size, :orig_len => self.to_s.size, :data => self) end |
#to_s ⇒ Object
Get the binary string of the entire packet.
62 63 64 |
# File 'lib/packetfu/packet.rb', line 62 def to_s @headers[0].to_s end |
#to_w(iface = nil) ⇒ Object
Put the entire packet on the wire by creating a temporary PacketFu::Inject object. TODO: Do something with auto-checksumming?
109 110 111 112 113 114 |
# File 'lib/packetfu/packet.rb', line 109 def to_w(iface=nil) iface = (iface || self.iface || PacketFu::Config.new.config[:iface]).to_s inj = PacketFu::Inject.new(:iface => iface) inj.array = [@headers[0].to_s] inj.inject end |
#write(io) ⇒ Object
In the event of no proper decoding, at least send it to the inner-most header.
67 68 69 |
# File 'lib/packetfu/packet.rb', line 67 def write(io) @headers[0].write(io) end |