Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

pcapr.Local

  • Loading branch information...
commit c9e0edbf75c1b93303347f89dbd4016fb3afc1ec 0 parents
@pcapr-local pcapr-local authored
Showing with 31,447 additions and 0 deletions.
  1. +5 −0 .document
  2. +43 −0 .gitignore
  3. +20 −0 LICENSE.txt
  4. +64 −0 README.md
  5. +57 −0 Rakefile
  6. +1 −0  VERSION
  7. +47 −0 bin/pcap2par
  8. +38 −0 bin/startpcapr
  9. +31 −0 bin/stoppcapr
  10. +5 −0 bin/xtractr
  11. +106 −0 lib/environment.rb
  12. BIN  lib/exe/xtractr
  13. +110 −0 lib/mu/pcap.rb
  14. +148 −0 lib/mu/pcap/ethernet.rb
  15. +75 −0 lib/mu/pcap/header.rb
  16. +67 −0 lib/mu/pcap/io_pair.rb
  17. +76 −0 lib/mu/pcap/io_wrapper.rb
  18. +61 −0 lib/mu/pcap/ip.rb
  19. +257 −0 lib/mu/pcap/ipv4.rb
  20. +148 −0 lib/mu/pcap/ipv6.rb
  21. +104 −0 lib/mu/pcap/packet.rb
  22. +155 −0 lib/mu/pcap/pkthdr.rb
  23. +61 −0 lib/mu/pcap/reader.rb
  24. +170 −0 lib/mu/pcap/reader/http_family.rb
  25. +367 −0 lib/mu/pcap/sctp.rb
  26. +123 −0 lib/mu/pcap/sctp/chunk.rb
  27. +134 −0 lib/mu/pcap/sctp/chunk/data.rb
  28. +100 −0 lib/mu/pcap/sctp/chunk/init.rb
  29. +68 −0 lib/mu/pcap/sctp/chunk/init_ack.rb
  30. +110 −0 lib/mu/pcap/sctp/parameter.rb
  31. +48 −0 lib/mu/pcap/sctp/parameter/ip_address.rb
  32. +72 −0 lib/mu/pcap/stream_packetizer.rb
  33. +505 −0 lib/mu/pcap/tcp.rb
  34. +69 −0 lib/mu/pcap/udp.rb
  35. +164 −0 lib/mu/scenario/pcap.rb
  36. +50 −0 lib/mu/scenario/pcap/fields.rb
  37. +71 −0 lib/mu/scenario/pcap/rtp.rb
  38. +169 −0 lib/pcapr_local.rb
  39. +291 −0 lib/pcapr_local/config.rb
  40. +197 −0 lib/pcapr_local/db.rb
  41. +250 −0 lib/pcapr_local/scanner.rb
  42. +178 −0 lib/pcapr_local/server.rb
  43. BIN  lib/pcapr_local/www/favicon.ico
  44. BIN  lib/pcapr_local/www/favicon.png
  45. +138 −0 lib/pcapr_local/www/home/index.html
  46. BIN  lib/pcapr_local/www/static/image/16x16/Cancel.png
  47. BIN  lib/pcapr_local/www/static/image/16x16/Cancel.png.1
  48. BIN  lib/pcapr_local/www/static/image/16x16/Download.png
  49. BIN  lib/pcapr_local/www/static/image/16x16/Folder3.png
  50. BIN  lib/pcapr_local/www/static/image/16x16/Full Size.png
  51. BIN  lib/pcapr_local/www/static/image/16x16/Minus.png
  52. BIN  lib/pcapr_local/www/static/image/16x16/Plus.png
  53. BIN  lib/pcapr_local/www/static/image/16x16/Search.png
  54. BIN  lib/pcapr_local/www/static/image/16x16/User.png
  55. BIN  lib/pcapr_local/www/static/image/48x48/Phone.png
  56. BIN  lib/pcapr_local/www/static/image/48x48/Video.png
  57. BIN  lib/pcapr_local/www/static/image/bar-orange.gif
  58. BIN  lib/pcapr_local/www/static/image/beta.png
  59. BIN  lib/pcapr_local/www/static/image/bg.png
  60. BIN  lib/pcapr_local/www/static/image/blockquote.png
  61. BIN  lib/pcapr_local/www/static/image/body-bg.png
  62. BIN  lib/pcapr_local/www/static/image/body-h3.png
  63. BIN  lib/pcapr_local/www/static/image/body-hl1-bg.png
  64. BIN  lib/pcapr_local/www/static/image/body-hl1-h3.png
  65. BIN  lib/pcapr_local/www/static/image/body-hl1-readmore.png
  66. BIN  lib/pcapr_local/www/static/image/body-hl2-bg.png
  67. BIN  lib/pcapr_local/www/static/image/body-hl2-h3.png
  68. BIN  lib/pcapr_local/www/static/image/body-hl2-readmore.png
  69. BIN  lib/pcapr_local/www/static/image/body-hl3-bg.png
  70. BIN  lib/pcapr_local/www/static/image/body-hl3-h3.png
  71. BIN  lib/pcapr_local/www/static/image/body-hl3-readmore.png
  72. BIN  lib/pcapr_local/www/static/image/body-hl4-bg.png
  73. BIN  lib/pcapr_local/www/static/image/body-hl4-h3.png
  74. BIN  lib/pcapr_local/www/static/image/body-hl4-readmore.png
  75. BIN  lib/pcapr_local/www/static/image/body-hl5-h3.png
  76. BIN  lib/pcapr_local/www/static/image/body-hl6-h3.png
  77. BIN  lib/pcapr_local/www/static/image/body-hl7-h3.png
  78. BIN  lib/pcapr_local/www/static/image/body-hl8-h3.png
  79. BIN  lib/pcapr_local/www/static/image/body-readmore.png
  80. BIN  lib/pcapr_local/www/static/image/bottom-bg.png
  81. BIN  lib/pcapr_local/www/static/image/bottom-l.png
  82. BIN  lib/pcapr_local/www/static/image/bottom-r.png
  83. BIN  lib/pcapr_local/www/static/image/btn-search.png
  84. BIN  lib/pcapr_local/www/static/image/bullet-1.png
  85. BIN  lib/pcapr_local/www/static/image/bullet-2.png
  86. BIN  lib/pcapr_local/www/static/image/bullet-3.png
  87. BIN  lib/pcapr_local/www/static/image/bullet-4.png
  88. BIN  lib/pcapr_local/www/static/image/bullet-5.png
  89. BIN  lib/pcapr_local/www/static/image/bullet-6.png
  90. BIN  lib/pcapr_local/www/static/image/bullet-7.png
  91. BIN  lib/pcapr_local/www/static/image/bullet-hl1.png
  92. BIN  lib/pcapr_local/www/static/image/bullet-hl2.png
  93. BIN  lib/pcapr_local/www/static/image/bullet-hl3.png
  94. BIN  lib/pcapr_local/www/static/image/bullet-hl4.png
  95. BIN  lib/pcapr_local/www/static/image/bullet-pathway.png
  96. BIN  lib/pcapr_local/www/static/image/bullet-section1.png
  97. BIN  lib/pcapr_local/www/static/image/bullet-section2.png
  98. BIN  lib/pcapr_local/www/static/image/collapsed.gif
  99. BIN  lib/pcapr_local/www/static/image/crosslink.png
  100. BIN  lib/pcapr_local/www/static/image/expanded.gif
  101. BIN  lib/pcapr_local/www/static/image/favicon.ico
  102. BIN  lib/pcapr_local/www/static/image/favicon.png
  103. BIN  lib/pcapr_local/www/static/image/icon-author.png
  104. BIN  lib/pcapr_local/www/static/image/icon-created.png
  105. BIN  lib/pcapr_local/www/static/image/p-expand.gif
  106. BIN  lib/pcapr_local/www/static/image/pcapr-logo.png
  107. BIN  lib/pcapr_local/www/static/image/powered-by.png
  108. BIN  lib/pcapr_local/www/static/image/section1-bg.png
  109. BIN  lib/pcapr_local/www/static/image/section1-h3.png
  110. BIN  lib/pcapr_local/www/static/image/section1-readmore.png
  111. BIN  lib/pcapr_local/www/static/image/section2-bg.png
  112. BIN  lib/pcapr_local/www/static/image/section2-h3.png
  113. BIN  lib/pcapr_local/www/static/image/section2-readmore.png
  114. BIN  lib/pcapr_local/www/static/image/status-alert.png
  115. BIN  lib/pcapr_local/www/static/image/status-download.png
  116. BIN  lib/pcapr_local/www/static/image/status-info.png
  117. BIN  lib/pcapr_local/www/static/image/status-note.png
  118. BIN  lib/pcapr_local/www/static/image/tab-round.png
  119. BIN  lib/pcapr_local/www/static/image/throbber.gif
  120. BIN  lib/pcapr_local/www/static/image/user.jpg
  121. +421 −0 lib/pcapr_local/www/static/script/closet/async.js
  122. +241 −0 lib/pcapr_local/www/static/script/closet/closet.api.js
  123. +94 −0 lib/pcapr_local/www/static/script/closet/closet.folders.js
  124. +187 −0 lib/pcapr_local/www/static/script/closet/closet.js
  125. +219 −0 lib/pcapr_local/www/static/script/closet/closet.mr.js
  126. +359 −0 lib/pcapr_local/www/static/script/closet/closet.options.js
  127. +73 −0 lib/pcapr_local/www/static/script/closet/closet.quantity.js
  128. +205 −0 lib/pcapr_local/www/static/script/closet/closet.render.js
  129. +86 −0 lib/pcapr_local/www/static/script/closet/closet.report.js
  130. +135 −0 lib/pcapr_local/www/static/script/closet/closet.reports.http.js
  131. +163 −0 lib/pcapr_local/www/static/script/closet/closet.reports.overview.js
  132. +159 −0 lib/pcapr_local/www/static/script/closet/closet.reports.sip.js
  133. +72 −0 lib/pcapr_local/www/static/script/closet/closet.reports.tcp.js
  134. +263 −0 lib/pcapr_local/www/static/script/closet/closet.reports.visualize.js
  135. +40 −0 lib/pcapr_local/www/static/script/closet/closet.util.js
  136. +154 −0 lib/pcapr_local/www/static/script/jquery/jquery-1.4.2.min.js
  137. +10,921 −0 lib/pcapr_local/www/static/script/jquery/jquery-ui.js
  138. +2,123 −0 lib/pcapr_local/www/static/script/jquery/jquery.flot.js
  139. +184 −0 lib/pcapr_local/www/static/script/jquery/jquery.flot.selection.js
  140. +184 −0 lib/pcapr_local/www/static/script/jquery/jquery.flot.stack.js
  141. +643 −0 lib/pcapr_local/www/static/script/jquery/jquery.form.js
  142. +3 −0  lib/pcapr_local/www/static/script/jquery/jquery.jsonp.min.js
  143. +142 −0 lib/pcapr_local/www/static/script/jquery/jquery.menu.js
  144. +308 −0 lib/pcapr_local/www/static/script/jquery/jquery.suggest.js
  145. +203 −0 lib/pcapr_local/www/static/script/jquery/jquery.ui.core.js
  146. +629 −0 lib/pcapr_local/www/static/script/jquery/jquery.ui.slider.js
  147. +1,055 −0 lib/pcapr_local/www/static/script/jquery/jquery.ui.sortable.js
  148. +236 −0 lib/pcapr_local/www/static/script/jquery/jquery.ui.widget.js
  149. +481 −0 lib/pcapr_local/www/static/script/json2.js
  150. +115 −0 lib/pcapr_local/www/static/script/sammy/plugins/sammy.cache.js
  151. +117 −0 lib/pcapr_local/www/static/script/sammy/plugins/sammy.template.js
  152. +1,696 −0 lib/pcapr_local/www/static/script/sammy/sammy.js
  153. +104 −0 lib/pcapr_local/www/static/script/tipsy/jquery.tipsy.js
  154. +116 −0 lib/pcapr_local/www/static/style/c3p0.css
  155. +27 −0 lib/pcapr_local/www/static/style/jquery.suggest.css
  156. +1,113 −0 lib/pcapr_local/www/static/style/page.css
  157. +7 −0 lib/pcapr_local/www/static/style/tipsy.css
  158. +10 −0 lib/pcapr_local/www/templates/browse.services.template
  159. +77 −0 lib/pcapr_local/www/templates/browse.template
  160. +38 −0 lib/pcapr_local/www/templates/flows.template
  161. +63 −0 lib/pcapr_local/www/templates/pcap.template
  162. +35 −0 lib/pcapr_local/www/templates/sip.calls.template
  163. +6 −0 lib/pcapr_local/www/templates/statistics.template
  164. +179 −0 lib/pcapr_local/xtractr.rb
  165. +172 −0 lib/pcapr_local/xtractr/instance.rb
  166. +251 −0 test/mu/pcap/reader/tc_http_family.rb
  167. +71 −0 test/mu/pcap/tc_ethernet.rb
  168. +56 −0 test/mu/pcap/tc_header.rb
  169. +103 −0 test/mu/pcap/tc_ipv4.rb
  170. +83 −0 test/mu/pcap/tc_ipv6.rb
  171. +44 −0 test/mu/pcap/tc_packet.rb
  172. +58 −0 test/mu/pcap/tc_pair.rb
  173. +33 −0 test/mu/pcap/tc_pkthdr.rb
  174. +76 −0 test/mu/pcap/tc_reader.rb
  175. +426 −0 test/mu/pcap/tc_tcp.rb
  176. +33 −0 test/mu/pcap/tc_udp.rb
  177. +80 −0 test/mu/pcap/tc_wrapper.rb
  178. +67 −0 test/mu/scenario/pcap/tc_fields.rb
  179. +135 −0 test/mu/scenario/pcap/tc_rtp.rb
  180. BIN  test/mu/scenario/sip_signalled_call_1.pcap
  181. +190 −0 test/mu/scenario/tc_pcap.rb
  182. BIN  test/mu/scenario/test_data/arp.pcap
  183. BIN  test/mu/scenario/test_data/dns.pcap
  184. BIN  test/mu/scenario/test_data/http-v6.pcap
  185. BIN  test/mu/scenario/test_data/http.pcap
  186. BIN  test/mu/scenario/test_data/http_chunked.pcap
  187. BIN  test/mu/scenario/test_data/http_deflate.pcap
  188. BIN  test/mu/scenario/test_data/httpauth3.pcap
  189. BIN  test/mu/scenario/test_data/icmp.pcap
  190. BIN  test/mu/scenario/test_data/sip_signalled_call_1.pcap
  191. +39 −0 test/mu/tc_pcap.rb
  192. +86 −0 test/mu/testcase.rb
  193. BIN  test/pcapr_local/arp.pcap
  194. +3 −0  test/pcapr_local/data.js
  195. BIN  test/pcapr_local/http_chunked.pcap
  196. +181 −0 test/pcapr_local/tc_api.rb
  197. BIN  test/pcapr_local/test.tgz
  198. +241 −0 test/pcapr_local/test_scanner.rb
  199. +219 −0 test/pcapr_local/test_xtractr.rb
  200. +107 −0 test/pcapr_local/testcase.rb
  201. +25 −0 test/test_export_to_scenario.sh
  202. +29 −0 test/test_pcapr_local.rb
5 .document
@@ -0,0 +1,5 @@
+lib/**/*.rb
+bin/*
+-
+features/**/*.feature
+LICENSE.txt
43 .gitignore
@@ -0,0 +1,43 @@
+# rcov generated
+coverage
+
+# rdoc generated
+rdoc
+
+# yard generated
+doc
+.yardoc
+
+# bundler
+.bundle
+
+# jeweler generated
+pkg
+
+# Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore:
+#
+# * Create a file at ~/.gitignore
+# * Include files you want ignored
+# * Run: git config --global core.excludesfile ~/.gitignore
+#
+# After doing this, these files will be ignored in all your git projects,
+# saving you from having to 'pollute' every project you touch with them
+#
+# Not sure what to needs to be ignored for particular editors/OSes? Here's some ideas to get you started. (Remember, remove the leading # of the line)
+#
+# For MacOS:
+#
+#.DS_Store
+#
+# For TextMate
+#*.tmproj
+#tmtags
+#
+# For emacs:
+#*~
+#\#*
+#.\#*
+#
+# For vim:
+*.swp
+*.svn
20 LICENSE.txt
@@ -0,0 +1,20 @@
+Copyright (c) 2011 Mu Dynamics
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
64 README.md
@@ -0,0 +1,64 @@
+# pcapr.Local #
+
+## Introduction
+
+pcapr.Local is a tool for browsing and managing a large repository of packet captures (pcaps). After you tell pcapr.Local where your pcaps are located, it will index them automatically and let you navigate your collection in the comfort of your web browser. pcapr.Local builds on and integrates with [Xtractr](http://code.google.com/p/pcapr/wiki/Xtractr) so you can analyze your pcaps in the Xtractr web UI. The Xtractr web UI is hosted on pcapr.net but talks to a local Xtractr instance (managed by pcapr.Local) and your data never leaves your network.
+
+In addition to managing your pcaps, you can use pcapr.Local to leverage your custom wireshark dissectors when creating Scenarios in Mu Studio. PAR files (described below) created by pcapr.Local can be imported into Mu Studio just like a pcap, but Mu Studio will use your wireshark data to guide Scenario creation.
+
+## Dependencies
+
+### CouchDB
+CouchDB needs to be available. Either or local or remote installation will work. On Ubuntu/Debian you can install CouchDB with:
+
+ $ sudo apt-get install couchdb
+
+### Wireshark
+
+You need to have wireshark installed. In particular the command line "tshark" utility should be available.
+
+### Ruby
+
+Tested with Ruby 1.8.6, 1.8.7, and 1.9.2.
+
+## Supported environments
+
+Linux only. Sorry.
+
+## Running pcapr.Local
+
+1. Install the gem.
+2. Run the "startpcapr" executable that is installed with the gem:
+
+ $ startpcapr
+
+This will ask you some basic questions, and will record your answers in a config file at ~/.pcapr_local/config that will be used on subsequent invocations. After collecting configuration information, the server process will continue running in the background and you'll get your prompt back. If you like to keep an eye on what's going on you can tail the pcapr.Local log file with:
+
+ $ tail -F ~/pcapr.Local/log/server.log
+
+3. Add pcaps to the pcap directory you configured (default ~/pcapr.Local/pcaps) and wait a short while for them to be noticed and indexed (about a minute).
+4. Point your browser to http://localhost:8080 (or whatever you configured).
+5. If you want to stop the pcapr.Local server you can do so with:
+
+ $ stoppcapr
+
+## Creating PAR files
+
+A PAR file (Pcap ARchive) is a format that can be imported onto a Mu Studio to create a Scenario. For purposes of Scenario creation, a PAR file is equivalent to the starting pcap with a couple of exceptions:
+
+1. The PAR file contains wireshark dissection data from your local wireshark installation. This means you get the full benefits of any custom dissectors you may have.
+2. When you import a PAR you'll bypass the normal flow selection page and go directly to the Scenario editor.
+
+### In the GUI
+
+Select a pcap in the pcapr.Local browser. The page that opens has a link at the bottom that lets you download a PAR file for that pcap.
+
+### On the Command Line
+
+The gem bundles a CLI tool for creating PAR files called 'pcap2par'. Usage is very simple, just provide a path to your pcap:
+
+ $ pcap2par my_traffic.pcap
+
+This will create the PAR file called "export.par" in the current directory. You can optionally specify the output file as a second argument:
+
+ $ pcap2par my_traffic.pcap ~/par_files/my_traffic.par
57 Rakefile
@@ -0,0 +1,57 @@
+require 'rubygems'
+require 'rake'
+
+require 'jeweler'
+Jeweler::Tasks.new do |gem|
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
+ gem.name = "pcapr-local"
+ gem.homepage = "http://github.com/pcapr-local/pcapr-local"
+ gem.license = "MIT"
+ gem.summary = %Q{Manage your pcap collection}
+ gem.description = %Q{Index, Browse, and Query your vast pcap collection.}
+ gem.email = "nbaggott@gmail.com"
+ gem.authors = ["Mu Dynamics"]
+ gem.add_dependency "rest-client", ">= 1.6.1"
+ gem.add_dependency "couchrest", "~> 1.0.1"
+ gem.add_dependency "sinatra", "~> 1.1.0"
+ gem.add_dependency "json", ">= 1.4.6"
+ gem.add_dependency "thin", "~> 1.2.7"
+ gem.add_dependency "rack", "~> 1.2.1"
+ gem.add_dependency "rack-contrib", "~> 1.1.0"
+ # Include your dependencies below. Runtime dependencies are required when using your gem,
+ # and development dependencies are only needed for development (ie running rake tasks, tests, etc)
+ # gem.add_runtime_dependency 'jabber4r', '> 0.1'
+ # gem.add_development_dependency 'rspec', '> 1.2.3'
+ gem.add_development_dependency "shoulda", ">= 0"
+ gem.add_development_dependency "bundler", "~> 1.0.0"
+ gem.add_development_dependency "jeweler", "~> 1.5.2"
+ gem.add_development_dependency "rcov", ">= 0"
+
+end
+Jeweler::RubygemsDotOrgTasks.new
+
+require 'rake/testtask'
+Rake::TestTask.new(:test) do |test|
+ test.libs << 'lib' << 'test'
+ test.pattern = 'test/**/test_*.rb'
+ test.verbose = true
+end
+
+require 'rcov/rcovtask'
+Rcov::RcovTask.new(:rcov) do |test|
+ test.libs << 'test'
+ test.pattern = 'test/**/test_*.rb'
+ test.verbose = true
+end
+
+task :default => :test
+
+require 'rake/rdoctask'
+Rake::RDocTask.new do |rdoc|
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
+
+ rdoc.rdoc_dir = 'rdoc'
+ rdoc.title = "pcapr-local #{version}"
+ rdoc.rdoc_files.include('README*')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+end
1  VERSION
@@ -0,0 +1 @@
+0.1.8
47 bin/pcap2par
@@ -0,0 +1,47 @@
+#!/usr/bin/env ruby
+# Copyright (C) 2008 Mu Dynamics, Inc
+#
+# This program is confidential and proprietary to Mu Dynamics, Inc and
+# may not be reproduced, published or disclosed to others without its
+# authorization.
+
+libdir = File.dirname(__FILE__) + "/../lib"
+libdir = File.expand_path(libdir)
+$: << libdir
+
+require 'pcapr_local'
+require 'optparse'
+require 'mu/pcap'
+require 'mu/scenario/pcap'
+
+options = {
+ :isolate_l7 => false
+}
+
+opts = OptionParser.new do |opts|
+ opts.banner =
+ "Usage: pcap2par [options] <pcap> [export file]"
+ opts.on('-i', '--isolate', 'Include only TCP/UDP/SCTP traffic (excluding DNS, DHCP)') do
+ options[:isolate_l7] = true
+ end
+ opts.on_tail('-h', '--help', 'Show this message') do
+ puts opts
+ exit 0
+ end
+end
+
+argv = opts.parse!
+unless argv.size == 1 or argv.size == 2
+ $stderr.puts opts
+ exit 1
+end
+
+pcap = argv[0]
+archive = argv[1] || "export.par"
+io = Mu::Scenario::Pcap.export_to_par pcap, options
+archive_io = open(archive, 'wb')
+while block=io.read(4096)
+ archive_io.print block
+end
+
+puts "export is located at #{archive}"
38 bin/startpcapr
@@ -0,0 +1,38 @@
+#!/usr/bin/env ruby
+
+libdir = File.dirname(__FILE__) + "/../lib"
+libdir = File.expand_path(libdir)
+$: << libdir
+
+require 'pcapr_local'
+require 'optparse'
+
+config_file = nil
+debug_mode = false
+opts = OptionParser.new do |opts|
+ opts.banner = "Usage: #{$0} [-f config_file]"
+ opts.on('-f', '--config_file FILE', 'Config file') do |f|
+ config_file = f
+ end
+ opts.on('-d', '--debug_mode', 'Run in debug mode (server runs in foreground)') do
+ debug_mode = true
+ end
+ opts.on_tail('-h', '--help', 'Show this message') do
+ puts opts
+ exit 0
+ end
+end
+opts.parse!
+
+config = PcaprLocal::Config.config config_file
+if debug_mode
+ config["debug_mode"] = true
+ # log to stdout
+ config["log_dir"] = nil
+else
+ config["debug_mode"] = false
+end
+
+
+PcaprLocal.start config
+
31 bin/stoppcapr
@@ -0,0 +1,31 @@
+#!/usr/bin/env ruby
+
+libdir = File.dirname(__FILE__) + "/../lib"
+libdir = File.expand_path(libdir)
+$: << libdir
+
+require 'pcapr_local'
+require 'optparse'
+
+include PcaprLocal
+
+config_file = nil
+opts = OptionParser.new do |opts|
+ opts.banner = "Usage: #{$0} [-f config_file]"
+ opts.on('-f', '--config_file FILE', 'Config file') do |f|
+ config_file = f
+ end
+ opts.on_tail('-h', '--help', 'Show this message') do
+ puts opts
+ exit 0
+ end
+end
+opts.parse!
+
+config_file ||= PcaprLocal::Config.user_config_path
+
+if File.exist?(config_file)
+ config = PcaprLocal::Config.config config_file
+ PcaprLocal.stop config
+end
+
5 bin/xtractr
@@ -0,0 +1,5 @@
+#!/usr/bin/env ruby
+
+xtractr = File.dirname(__FILE__) + "/../lib/exe/xtractr"
+xtractr = File.expand_path xtractr
+exec xtractr, *ARGV
106 lib/environment.rb
@@ -0,0 +1,106 @@
+# http://www.mudynamics.com
+# http://labs.mudynamics.com
+# http://www.pcapr.net
+
+if defined? Encoding
+ Encoding.default_external = Encoding::BINARY
+end
+
+module PcaprLocal
+ ROOT = File.expand_path(File.dirname(File.dirname(__FILE__)))
+ $: << ROOT
+end
+
+class Integer
+ # Make sure Integer#ord is present
+ if RUBY_VERSION < "1.8.7"
+ def ord
+ return self
+ end
+ end
+end
+
+# Make sure barebones Dir.mktmpdir is present
+require 'tempfile'
+class Dir
+ if not self.respond_to? :mktmpdir
+ def self.mktmpdir
+ t = (Time.now.to_f * 1_000_000).to_i.to_s(36)
+ path = "#{tmpdir}/d#{t}-#{$$}-#{rand(0x100000000).to_s(36)}"
+ Dir.mkdir path
+ path
+ end
+ end
+end
+
+
+module Process
+ # Supply daemon for pre ruby 1.9
+ # Adapted from lib/active_support/core_ext/process/daemon.rb
+ def self.daemon(nochdir = nil, noclose = nil)
+ exit! if fork # Parent exits, child continues.
+ Process.setsid # Become session leader.
+ exit! if fork # Zap session leader. See [1].
+
+ unless nochdir
+ Dir.chdir "/" # Release old working directory.
+ end
+
+ unless noclose
+ STDIN.reopen "/dev/null" # Free file descriptors and
+ STDOUT.reopen "/dev/null", "a" # point them somewhere sensible.
+ STDERR.reopen '/dev/null', 'a'
+ end
+
+ trap("TERM") { exit }
+
+ return 0
+
+ end unless self.respond_to? :daemon
+end
+
+class Regexp
+ # Patch Regexp.union to accept an array
+ if RUBY_VERSION < "1.8.7"
+ class << self
+ alias :union_pre187 :union
+ def union *arg
+ if arg.size == 1 and arg[0].is_a? Array
+ arg = arg[0]
+ end
+ union_pre187 *arg
+ end
+ end
+ end
+end
+
+class String
+ # Convert from hex. E.g. "0d0a".from_hex is "\r\n".
+ # Raises ArgumentError on invalid input.
+ def from_hex
+ return "" if self.empty?
+ hex = self
+ Integer("0x#{hex}")
+ if hex.length % 2 == 1
+ hex = "0#{hex}"
+ end
+ [hex].pack 'H*'
+ end
+end
+
+# Implement simple Readline.readline if interpreter is not
+# compiled with readline support.
+begin
+ require 'readline'
+rescue LoadError
+ class Readline
+ def self.readline prompt
+ print prompt
+ gets
+ end
+ end
+end
+
+
+
+
BIN  lib/exe/xtractr
Binary file not shown
110 lib/mu/pcap.rb
@@ -0,0 +1,110 @@
+# http://www.mudynamics.com
+# http://labs.mudynamics.com
+# http://www.pcapr.net
+
+require 'socket'
+require 'stringio'
+
+module Mu
+
+class Pcap
+ class ParseError < StandardError ; end
+
+ LITTLE_ENDIAN = 0xd4c3b2a1
+ BIG_ENDIAN = 0xa1b2c3d4
+
+ DLT_NULL = 0
+ DLT_EN10MB = 1
+ DLT_RAW = 12 # DLT_LOOP in OpenBSD
+ DLT_LINUX_SLL = 113
+
+ attr_accessor :header, :pkthdrs
+
+ def initialize
+ @header = Header.new
+ @pkthdrs = []
+ end
+
+ # Read PCAP file from IO and return Mu::Pcap. If decode is true, also
+ # decode the Pkthdr packet contents to Mu::Pcap objects.
+ def self.read io, decode=true
+ pcap = Pcap.new
+ pcap.header = each_pkthdr(io, decode) do |pkthdr|
+ pcap.pkthdrs << pkthdr
+ end
+ return pcap
+ end
+
+ # Create PCAP from list of packets.
+ def self.from_packets packets
+ pcap = Pcap.new
+ packets.each do |packet|
+ pkthdr = Mu::Pcap::Pkthdr.new
+ pkthdr.pkt = packet
+ pcap.pkthdrs << pkthdr
+ end
+ return pcap
+ end
+
+ # Write PCAP file to IO. Uses big-endian and linktype EN10MB.
+ def write io
+ @header.write io
+ @pkthdrs.each do |pkthdr|
+ pkthdr.write io
+ end
+ end
+
+ # Read PCAP packet headers from IO and return Mu::Pcap::Header. If decode
+ # is true, also decode the Pkthdr packet contents to Mu::Pcap objects. Use
+ # this for large files when each packet header can processed independently
+ # - it will perform better.
+ def self.each_pkthdr io, decode=true
+ header = Header.read io
+ while not io.eof?
+ pkthdr = Pkthdr.read io, header.magic
+ if decode
+ pkthdr.decode! header.magic, header.linktype
+ end
+ yield pkthdr
+ end
+ return header
+ end
+
+ # Read packets from PCAP
+ def self.read_packets io, decode=true
+ packets = []
+ each_pkthdr(io) { |pkthdr| packets << pkthdr.pkt }
+ return packets
+ end
+
+ # Assertion used during Pcap parsing
+ def self.assert cond, msg
+ if not cond
+ raise ParseError, msg
+ end
+ end
+
+ # Warnings from Pcap parsing are printed using this method.
+ def self.warning msg
+ $stderr.puts "WARNING: #{msg}"
+ end
+
+ def == other
+ return self.class == other.class &&
+ self.header == other.header &&
+ self.pkthdrs == other.pkthdrs
+ end
+end
+
+end
+
+require 'mu/pcap/header'
+require 'mu/pcap/pkthdr'
+require 'mu/pcap/packet'
+require 'mu/pcap/ethernet'
+require 'mu/pcap/ip'
+require 'mu/pcap/ipv4'
+require 'mu/pcap/ipv6'
+require 'mu/pcap/tcp'
+require 'mu/pcap/udp'
+require 'mu/pcap/sctp'
148 lib/mu/pcap/ethernet.rb
@@ -0,0 +1,148 @@
+# http://www.mudynamics.com
+# http://labs.mudynamics.com
+# http://www.pcapr.net
+
+module Mu
+class Pcap
+
+class Ethernet < Packet
+ attr_accessor :src, :dst, :type
+
+ ETHERTYPE_IP = 0x0800
+ ETHERTYPE_IP6 = 0x86dd
+ ETHERTYPE_ARP = 0x0806
+ ETHERTYPE_PPPOE_SESSION = 0x8864
+ ETHERTYPE_802_1Q = 0X8100
+
+ PPP_IP = 0x0021
+ PPP_IPV6 = 0x0057
+
+ def initialize src=nil, dst=nil, type=0
+ super()
+ @src = src
+ @dst = dst
+ @type = type
+ end
+
+ def flow_id
+ if not @payload or @payload.is_a? String
+ return [:ethernet, @src, @dst, @type]
+ else
+ return @payload.flow_id
+ end
+ end
+
+ FMT_MAC = "C6"
+ FMT_n = 'n'
+ MAC_TEMPLATE = '%02x:%02x:%02x:%02x:%02x:%02x'
+ def self.from_bytes bytes
+ if bytes.length < 14
+ raise ParseError, "Truncated Ethernet header: expected 14 bytes, got #{bytes.length} bytes"
+ end
+
+ dst = bytes.slice!(0,6).unpack FMT_MAC
+ dst = MAC_TEMPLATE % dst
+ src = bytes.slice!(0,6).unpack FMT_MAC
+ src = MAC_TEMPLATE % src
+ type = bytes.slice!(0,2).unpack(FMT_n)[0]
+ while (type == ETHERTYPE_802_1Q)
+ # Skip 4 bytes for 802.1q vlan tag field
+ bytes.slice!(0,2)
+ type = bytes.slice!(0,2).unpack(FMT_n)[0]
+ end
+ ethernet = Ethernet.new src, dst, type
+ ethernet.payload = bytes
+ ethernet.payload_raw = bytes
+ begin
+ case type
+ when ETHERTYPE_IP
+ ethernet.payload = IPv4.from_bytes bytes
+ when ETHERTYPE_IP6
+ ethernet.payload = IPv6.from_bytes bytes
+ when ETHERTYPE_PPPOE_SESSION
+ # Remove PPPoE/PPP session layer
+ ethernet.payload = bytes
+ ethernet.remove_pppoe!
+ else
+ ethernet.payload = bytes
+ end
+ rescue ParseError => e
+ Pcap.warning e
+ end
+ return ethernet
+ end
+
+ def ip?
+ return payload.is_a?(IP)
+ end
+
+ ADDR_TO_BYTES = {}
+ FMT_HEADER = 'a6a6n'
+ def write io
+ dst_mac = ADDR_TO_BYTES[@dst] ||= @dst.split(':').inject('') {|m, b| m << b.to_i(16).chr}
+ src_mac = ADDR_TO_BYTES[@src] ||= @src.split(':').inject('') {|m, b| m << b.to_i(16).chr}
+ bytes = [dst_mac, src_mac, @type].pack(FMT_HEADER)
+ io.write bytes
+ if @payload.is_a? String
+ io.write @payload
+ else
+ @payload.write io
+ end
+ end
+
+ # Remove the PPPoE and PPP headers. PPPoE is documented in RFC 2516.
+ def remove_pppoe!
+ bytes = self.payload_raw
+
+ # Remove PPPoE header
+ Pcap.assert bytes.length >= 6, 'Truncated PPPoE header: ' +
+ "expected at least 6 bytes, got #{bytes.length} bytes"
+ version_type, code, session_id, length = bytes.unpack 'CCnn'
+ version = version_type >> 4 & 0b1111
+ type = version_type & 0b1111
+ Pcap.assert version == 1, "Unknown PPPoE version: #{version}"
+ Pcap.assert type == 1, "Unknown PPPoE type: #{type}"
+ Pcap.assert code == 0, "Unknown PPPoE code: #{code}"
+ bytes = bytes[6..-1]
+ Pcap.assert bytes.length >= length, 'Truncated PPoE packet: ' +
+ "expected #{length} bytes, got #{bytes.length} bytes"
+
+ # Remove PPP header
+ Pcap.assert bytes.length >= 2, 'Truncated PPP packet: ' +
+ "expected at least bytes, got #{bytes.length} bytes"
+ protocol_id, = bytes.unpack 'n'
+ bytes = bytes[2..-1]
+ case protocol_id
+ when PPP_IP
+ self.payload = IPv4.from_bytes bytes
+ self.payload_raw = bytes
+ self.type = ETHERTYPE_IP
+ when PPP_IPV6
+ self.payload = IPv6.from_bytes bytes
+ self.payload_raw = bytes
+ self.type = ETHERTYPE_IP6
+ else
+ # Failed. Don't update payload or type.
+ raise ParseError, "Unknown PPP protocol: 0x#{'%04x' % protocol_id}"
+ end
+ end
+
+ def to_s
+ if @payload.is_a? String
+ payload = @payload.inspect
+ else
+ payload = @payload.to_s
+ end
+ return "ethernet(%s, %s, %d, %s)" % [@src, @dst, @type, payload]
+ end
+
+ def == other
+ return super &&
+ self.src == other.src &&
+ self.dst == other.dst &&
+ self.type == other.type
+ end
+end
+
+end
+end
75 lib/mu/pcap/header.rb
@@ -0,0 +1,75 @@
+# http://www.mudynamics.com
+# http://labs.mudynamics.com
+# http://www.pcapr.net
+
+module Mu
+class Pcap
+
+class Header
+ attr_accessor :magic, :version_major, :version_minor, :thiszone, :sigfigs,
+ :snaplen, :linktype
+
+ BIG_ENDIAN_FORMAT = 'nnNNNN'
+ LITTLE_ENDIAN_FORMAT = 'vvVVVV'
+
+ UNSUPPORTED_FORMATS = {
+ 0x474D4255 => "NetMon", # "GMBU"
+ 0x5452534E => "NA Sniffer (DOS)" # Starts with "TRSNIFF data"
+ }
+
+ def initialize
+ @magic = BIG_ENDIAN
+ @version_major = 2
+ @version_minor = 4
+ @thiszone = 0
+ @sigfigs = 0
+ @snaplen = 1500
+ @linktype = DLT_NULL
+ end
+
+ def self.read ios
+ header = Header.new
+ bytes = ios.read 24
+ Pcap.assert bytes, 'PCAP header missing'
+ Pcap.assert bytes.length == 24, 'Truncated PCAP header: ' +
+ "expected 24 bytes, got #{bytes.length} bytes"
+ header.magic, _ = bytes[0, 4].unpack 'N'
+ if header.magic == BIG_ENDIAN
+ format = BIG_ENDIAN_FORMAT
+ elsif header.magic == LITTLE_ENDIAN
+ format = LITTLE_ENDIAN_FORMAT
+ else
+ format = UNSUPPORTED_FORMATS[header.magic]
+ if format.nil?
+ err = "Unsupported packet capture format. "
+ else
+ err = "#{format} capture files are not supported. "
+ end
+ raise ParseError, err
+ end
+ header.version_major, header.version_minor, header.thiszone,
+ header.sigfigs, header.snaplen, header.linktype =
+ bytes[4..-1].unpack format
+ return header
+ end
+
+ def write io
+ bytes = [BIG_ENDIAN, @version_major, @version_minor, @thiszone,
+ @sigfigs, @snaplen, DLT_EN10MB].pack('N' + BIG_ENDIAN_FORMAT)
+ io.write bytes
+ end
+
+ def == other
+ return self.class == other.class &&
+ self.magic == other.magic &&
+ self.version_major == other.version_major &&
+ self.version_minor == other.version_minor &&
+ self.thiszone == other.thiszone &&
+ self.sigfigs == other.sigfigs &&
+ self.snaplen == other.snaplen &&
+ self.linktype == other.linktype
+ end
+end
+
+end
+end
67 lib/mu/pcap/io_pair.rb
@@ -0,0 +1,67 @@
+# http://www.mudynamics.com
+# http://labs.mudynamics.com
+# http://www.pcapr.net
+
+module Mu
+class Pcap
+
+# For emulating of a pair of connected sockets. Bytes written
+# with #write to one side are returned by a subsequent #read on
+# the other side.
+#
+# Use Pair.stream_pair to get a pair with stream semantics.
+# Use Pair.packet_pair to get a pair with packet semantics.
+class IOPair
+ attr_reader :read_queue
+ attr_accessor :other
+
+ def initialize
+ raise NotImplementedError
+ end
+
+ def self.stream_pair
+ io1 = Stream.new
+ io2 = Stream.new
+ io1.other = io2
+ io2.other = io1
+ return io1, io2
+ end
+
+ def self.packet_pair
+ io1 = Packet.new
+ io2 = Packet.new
+ io1.other = io2
+ io2.other = io1
+ return io1, io2
+ end
+
+ def write bytes
+ @other.read_queue << bytes
+ bytes.size
+ end
+
+ class Stream < IOPair
+ def initialize
+ @read_queue = ""
+ end
+
+ def read n=nil
+ n ||= @read_queue.size
+ @read_queue.slice!(0,n)
+ end
+ end
+
+ class Packet < IOPair
+ def initialize
+ @read_queue = []
+ end
+
+ def read
+ @read_queue.shift
+ end
+ end
+
+end
+end
+end
+
76 lib/mu/pcap/io_wrapper.rb
@@ -0,0 +1,76 @@
+# http://www.mudynamics.com
+# http://labs.mudynamics.com
+# http://www.pcapr.net
+
+require 'mu/pcap/io_pair'
+
+module Mu
+class Pcap
+class IOWrapper
+ attr_reader :ios, :unread, :state
+
+ def initialize ios, reader
+ @ios = ios
+ @reader = reader
+ # parse state for reader
+ @state = {}
+ # read off of underlying io but not yet processed by @reader
+ @unread = ""
+ end
+
+ # Impose upper limit to protect against memory exhaustion.
+ MAX_RECEIVE_SIZE = 1048576 # 1MB
+
+ # Returns next higher level protocol message.
+ def read
+ until message = @reader.read_message!(@unread, @state)
+ bytes = @ios.read
+ if bytes and not bytes.empty?
+ @unread << bytes
+ else
+ return nil
+ end
+ if @unread.size > MAX_RECEIVE_SIZE
+ raise "Maximum message size (#{MAX_RECEIVE_SIZE}) exceeded"
+ end
+ end
+
+ return message
+ end
+
+ # Parser may need to see requests to understand responses.
+ def record_write bytes
+ @reader.record_write bytes, @state
+ end
+
+ def write bytes, *args
+ w = @ios.write bytes, *args
+ record_write bytes
+ w
+ end
+
+ def write_to bytes, *args
+ w = @ios.write_to bytes, *args
+ record_write bytes
+ w
+ end
+
+ def open
+ if block_given?
+ @ios.open { yield }
+ else
+ @ios.open
+ end
+ end
+
+ def open?
+ @ios.open?
+ end
+
+ def close
+ @ios.close
+ end
+
+end
+end
+end
61 lib/mu/pcap/ip.rb
@@ -0,0 +1,61 @@
+# http://www.mudynamics.com
+# http://labs.mudynamics.com
+# http://www.pcapr.net
+
+module Mu
+class Pcap
+
+class IP < Packet
+ IPPROTO_TCP = 6
+ IPPROTO_UDP = 17
+ IPPROTO_HOPOPTS = 0
+ IPPROTO_ROUTING = 43
+ IPPROTO_FRAGMENT = 44
+ IPPROTO_AH = 51
+ IPPROTO_NONE = 59
+ IPPROTO_DSTOPTS = 60
+ IPPROTO_SCTP = 132
+
+ attr_accessor :src, :dst
+
+ def initialize src=nil, dst=nil
+ super()
+ @src = src
+ @dst = dst
+ end
+
+ def v4?
+ return false
+ end
+
+ def v6?
+ return false
+ end
+
+ def proto
+ raise NotImplementedError
+ end
+
+ def pseudo_header payload_length
+ raise NotImplementedError
+ end
+
+ def == other
+ return super &&
+ self.src == other.src &&
+ self.dst == other.dst
+ end
+
+ def self.checksum bytes
+ if bytes.size & 1 == 1
+ bytes = bytes + "\0"
+ end
+ sum = 0
+ bytes.unpack("n*").each {|n| sum += n }
+ sum = (sum & 0xffff) + (sum >> 16 & 0xffff)
+ ~sum & 0xffff
+ end
+end
+
+end
+end
257 lib/mu/pcap/ipv4.rb
@@ -0,0 +1,257 @@
+# http://www.mudynamics.com
+# http://labs.mudynamics.com
+# http://www.pcapr.net
+
+require 'ipaddr'
+
+module Mu
+class Pcap
+
+class IPv4 < IP
+ IP_RF = 0x8000 # Reserved
+ IP_DF = 0x4000 # Don't fragment
+ IP_MF = 0x2000 # More fragments
+ IP_OFFMASK = 0x1fff
+
+ FMT_HEADER = 'CCnnnCCna4a4'
+
+ attr_accessor :ip_id, :offset, :ttl, :proto, :src, :dst, :dscp
+
+ def initialize src=nil, dst=nil, ip_id=0, offset=0, ttl=64, proto=0, dscp=0
+ super()
+ @ip_id = ip_id
+ @offset = offset
+ @ttl = ttl
+ @proto = proto
+ @src = src
+ @dst = dst
+ @dscp = dscp
+ end
+
+ def v4?
+ return true
+ end
+
+ def flow_id
+ if not @payload or @payload.is_a? String
+ return [:ipv4, @proto, @src, @dst]
+ else
+ return [:ipv4, @src, @dst, @payload.flow_id]
+ end
+ end
+
+ NTOP = {} # Network to human cache
+ HTON = {} # Human to network cache
+
+ def self.from_bytes bytes
+ bytes.length >= 20 or
+ raise ParseError, "Truncated IPv4 header: expected at least 20 bytes, got #{bytes.length} bytes"
+
+ vhl, tos, length, id, offset, ttl, proto, checksum, src, dst = bytes[0,20].unpack FMT_HEADER
+ version = vhl >> 4
+ hl = (vhl & 0b1111) * 4
+
+ version == 4 or
+ raise ParseError, "Wrong IPv4 version: got (#{version})"
+ hl >= 20 or
+ raise ParseError, "Bad IPv4 header length: expected at least 20 bytes raise ParseError, got #{hl} bytes"
+ bytes.length >= hl or
+ raise ParseError, "Truncated IPv4 header: expected #{hl} bytes raise ParseError, got #{bytes.length} bytes"
+ length >= 20 or
+ raise ParseError, "Bad IPv4 packet length: expected at least 20 bytes raise ParseError, got #{length} bytes"
+ bytes.length >= length or
+ raise ParseError, "Truncated IPv4 packet: expected #{length} bytes raise ParseError, got #{bytes.length} bytes"
+
+ if hl != 20
+ IPv4.check_options bytes[20, hl-20]
+ end
+
+ src = NTOP[src] ||= IPAddr.ntop(src)
+ dst = NTOP[dst] ||= IPAddr.ntop(dst)
+ dscp = tos >> 2
+ ipv4 = IPv4.new(src, dst, id, offset, ttl, proto, dscp)
+ ipv4.payload_raw = bytes[hl..-1]
+
+ payload = bytes[hl...length]
+ if offset & (IP_OFFMASK | IP_MF) == 0
+ begin
+ case proto
+ when IPPROTO_TCP
+ ipv4.payload = TCP.from_bytes payload
+ when IPPROTO_UDP
+ ipv4.payload = UDP.from_bytes payload
+ when IPPROTO_SCTP
+ ipv4.payload = SCTP.from_bytes payload
+ else
+ ipv4.payload = payload
+ end
+ rescue ParseError => e
+ Pcap.warning e
+ end
+ else
+ ipv4.payload = payload
+ end
+ return ipv4
+ end
+
+ def write io
+ if @payload.is_a? String
+ payload = @payload
+ else
+ string_io = StringIO.new
+ @payload.write string_io, self
+ payload = string_io.string
+ end
+ length = 20 + payload.length
+ if length > 65535
+ Pcap.warning "IPv4 payload is too large"
+ end
+
+ src = HTON[@src] ||= IPAddr.new(@src).hton
+ dst = HTON[@dst] ||= IPAddr.new(@dst).hton
+ fields = [0x45, @dscp << 2, length, @ip_id, @offset, @ttl, @proto, 0, src, dst]
+ header = fields.pack(FMT_HEADER)
+ fields[7] = IP.checksum(header)
+ header = fields.pack(FMT_HEADER)
+ io.write header
+ io.write payload
+ end
+
+ FMT_PSEUDO_HEADER = 'a4a4CCn'
+ def pseudo_header payload_length
+ src = HTON[@src] ||= IPAddr.new(@src).hton
+ dst = HTON[@dst] ||= IPAddr.new(@dst).hton
+ return [src, dst, 0, @proto, payload_length].pack(FMT_PSEUDO_HEADER)
+ end
+
+ def fragment?
+ return (@offset & (IP_OFFMASK | IP_MF) != 0)
+ end
+
+ # Check that IP or TCP options are valid. Do nothing if they are valid.
+ # Both IP and TCP options are 8-bit TLVs with an inclusive length. Both
+ # have one byte options 0 and 1.
+ def self.check_options options, label='IPv4'
+ while not options.empty?
+ type = options.slice!(0, 1)[0].ord
+ if type == 0 or type == 1
+ next
+ end
+ Pcap.assert !options.empty?,
+ "#{label} option #{type} is missing the length field"
+ length = options.slice!(0, 1)[0].ord
+ Pcap.assert length >= 2,
+ "#{label} option #{type} has invalid length: #{length}"
+ Pcap.assert length - 2 <= options.length,
+ "#{label} option #{type} has truncated data"
+ options.slice! 0, length - 2
+ end
+ end
+
+ ReassembleState = ::Struct.new :packets, :bytes, :mf, :overlap
+
+ # Reassemble fragmented IPv4 packets
+ def self.reassemble packets
+ reassembled_packets = []
+ flow_id_to_state = {}
+ packets.each do |packet|
+ if not packet.is_a?(Ethernet) or not packet.payload.is_a?(IPv4)
+ # Ignore non-IPv4 packet
+ elsif not packet.payload.fragment?
+ # Ignore non-fragments
+ else
+ # Get reassembly state
+ ip = packet.payload
+ flow_id = [ip.ip_id, ip.proto, ip.src, ip.dst]
+ state = flow_id_to_state[flow_id]
+ if not state
+ state = ReassembleState.new [], [], true, false
+ flow_id_to_state[flow_id] = state
+ end
+ state.packets << packet
+
+ # Clear the more-fragments flag if no more fragments
+ if ip.offset & IP_MF == 0
+ state.mf = false
+ end
+
+ # Add the bytes
+ start = (ip.offset & IP_OFFMASK) * 8
+ finish = start + ip.payload.length
+ state.bytes.fill nil, start, finish - start
+ start.upto(finish-1) do |i|
+ if not state.bytes[i]
+ byte = ip.payload[i - start].chr
+ state.bytes[i] = byte
+ elsif not state.overlap
+ name = "%s:%s:%d" % [ip.src, ip.dst, ip.proto]
+ Pcap.warning \
+ "IPv4 flow #{name} contains overlapping fragements"
+ state.overlap = true
+ end
+ end
+
+ # We're done if we've received a fragment without the
+ # more-fragments flag and all the bytes in the buffer have been
+ # set.
+ if not state.mf and state.bytes.all?
+ # Remove fragments from reassembled_packets
+ state.packets.each do |packet|
+ reassembled_packets.delete_if do |reassembled_packet|
+ packet.object_id == reassembled_packet.object_id
+ end
+ end
+ # Remove state
+ flow_id_to_state.delete flow_id
+ # Create new packet
+ packet = state.packets[0].deepdup
+ ipv4 = packet.payload
+ ipv4.offset = 0
+ ipv4.payload = state.bytes.join
+ # Decode
+ begin
+ case ipv4.proto
+ when IPPROTO_TCP
+ ipv4.payload = TCP.from_bytes ipv4.payload
+ when IPPROTO_UDP
+ ipv4.payload = UDP.from_bytes ipv4.payload
+ when IPPROTO_SCTP
+ ipv4.payload = SCTP.from_bytes ipv4.payload
+ end
+ rescue ParseError => e
+ Pcap.warning e
+ end
+ end
+ end
+ reassembled_packets << packet
+ end
+ if !flow_id_to_state.empty?
+ Pcap.warning \
+ "#{flow_id_to_state.length} flow(s) have IPv4 fragments " \
+ "that can't be reassembled"
+ end
+
+ return reassembled_packets
+ end
+
+ def to_s
+ if @payload.is_a? String
+ payload = @payload.inspect
+ else
+ payload = @payload.to_s
+ end
+ return "ipv4(%d, %s, %s, %s)" % [@proto, @src, @dst, payload]
+ end
+
+ def == other
+ return super &&
+ self.proto == other.proto &&
+ self.ip_id == other.ip_id &&
+ self.offset == other.offset &&
+ self.ttl == other.ttl &&
+ self.dscp == other.dscp
+ end
+end
+
+end
+end
148 lib/mu/pcap/ipv6.rb
@@ -0,0 +1,148 @@
+# http://www.mudynamics.com
+# http://labs.mudynamics.com
+# http://www.pcapr.net
+
+require 'ipaddr'
+
+module Mu
+class Pcap
+
+class IPv6 < IP
+ FORMAT = 'NnCCa16a16'
+
+ attr_accessor :next_header, :hop_limit
+
+ def initialize
+ super
+ @next_header = 0
+ @hop_limit = 64
+ end
+
+ def v6?
+ return true
+ end
+
+ alias :proto :next_header
+ alias :ttl :hop_limit
+
+ def flow_id
+ if not @payload or @payload.is_a? String
+ return [:ipv6, @next_header, @src, @dst]
+ else
+ return [:ipv6, @src, @dst, @payload.flow_id]
+ end
+ end
+
+ def self.from_bytes bytes
+ Pcap.assert bytes.length >= 40, 'Truncated IPv6 header: ' +
+ "expected at least 40 bytes, got #{bytes.length} bytes"
+
+ vcl, length, next_header, hop_limit, src, dst =
+ bytes[0, 40].unpack FORMAT
+ version = vcl >> 28 & 0x0f
+ traffic_class = vcl >> 20 & 0xff
+ flow_label = vcl & 0xfffff
+
+ Pcap.assert version == 6, "Wrong IPv6 version: got (#{version})"
+ Pcap.assert bytes.length >= (40 + length), 'Truncated IPv6 header: ' +
+ "expected #{length + 40} bytes, got #{bytes.length} bytes"
+
+ ipv6 = IPv6.new
+ ipv6.next_header = next_header
+ ipv6.hop_limit = hop_limit
+ ipv6.src = IPAddr.new_ntoh(src).to_s
+ ipv6.dst = IPAddr.new_ntoh(dst).to_s
+
+ ipv6.payload_raw = bytes[40..-1]
+ ipv6.next_header, ipv6.payload =
+ payload_from_bytes ipv6, ipv6.next_header, bytes[40...40+length]
+
+ return ipv6
+ end
+
+ # Parse bytes and returns next_header and payload. Skips extension
+ # headers.
+ def self.payload_from_bytes ipv6, next_header, bytes
+ begin
+ case next_header
+ when IPPROTO_TCP
+ payload = TCP.from_bytes bytes
+ when IPPROTO_UDP
+ payload = UDP.from_bytes bytes
+ when IPPROTO_SCTP
+ payload = SCTP.from_bytes bytes
+ when IPPROTO_HOPOPTS
+ next_header, payload = eight_byte_header_from_bytes(ipv6,
+ bytes, 'hop-by-hop options')
+ when IPPROTO_ROUTING
+ next_header, payload = eight_byte_header_from_bytes(ipv6,
+ bytes, 'routing')
+ when IPPROTO_DSTOPTS
+ next_header, payload = eight_byte_header_from_bytes(ipv6,
+ bytes, 'destination options')
+ when IPPROTO_FRAGMENT
+ Pcap.assert bytes.length >= 8,
+ "Truncated IPv6 fragment header"
+ Pcap.assert false, 'IPv6 fragments are not supported'
+ when IPPROTO_AH
+ next_header, payload = ah_header_from_bytes(ipv6,
+ bytes, 'authentication header')
+ when IPPROTO_NONE
+ payload = ''
+ else
+ payload = bytes
+ end
+ rescue ParseError => e
+ Pcap.warning e
+ payload = bytes
+ end
+ return [next_header, payload]
+ end
+
+ # Parse extension header that's a multiple of 8 bytes
+ def self.eight_byte_header_from_bytes ipv6, bytes, name
+ Pcap.assert bytes.length >= 8, "Truncated IPv6 #{name} header"
+ length = (bytes[1].ord + 1) * 8
+ Pcap.assert bytes.length >= length, "Truncated IPv6 #{name} header"
+ return payload_from_bytes(ipv6, bytes[0].ord, bytes[length..-1])
+ end
+
+ # Parse authentication header (whose length field is interpeted differently)
+ def self.ah_header_from_bytes ipv6, bytes, name
+ Pcap.assert bytes.length >= 8, "Truncated IPv6 #{name} header"
+ length = (bytes[1].ord + 2) * 4
+ Pcap.assert bytes.length >= length, "Truncated IPv6 #{name} header"
+ return payload_from_bytes(ipv6, bytes[0].ord, bytes[length..-1])
+ end
+
+ def write io
+ if @payload.is_a? String
+ payload = @payload
+ else
+ string_io = StringIO.new
+ @payload.write string_io, self
+ payload = string_io.string
+ end
+ src = IPAddr.new(@src, Socket::AF_INET6).hton
+ dst = IPAddr.new(@dst, Socket::AF_INET6).hton
+ header = [0x60000000, payload.length, @next_header, @hop_limit,
+ src, dst].pack FORMAT
+ io.write header
+ io.write payload
+ end
+
+ def pseudo_header payload_length
+ return IPAddr.new(@src, Socket::AF_INET6).hton +
+ IPAddr.new(@dst, Socket::AF_INET6).hton +
+ [payload_length, '', @next_header].pack('Na3C')
+ end
+
+ def == other
+ return super &&
+ self.next_header == other.next_header &&
+ self.hop_limit == other.hop_limit
+ end
+end
+
+end
+end
104 lib/mu/pcap/packet.rb
@@ -0,0 +1,104 @@
+# http://www.mudynamics.com
+# http://labs.mudynamics.com
+# http://www.pcapr.net
+
+module Mu
+class Pcap
+
+class Packet
+ attr_accessor :payload, :payload_raw
+
+ def initialize
+ @payload = ''
+ @payload_raw = ''
+ end
+
+ # Get payload as bytes. If the payload is a parsed object, returns
+ # raw payload. Otherwise return unparsed bytes.
+ def payload_bytes
+ if @payload.is_a? String
+ return @payload
+ end
+ return @payload_raw
+ end
+
+ def deepdup
+ dup = self.dup
+ if @payload.respond_to? :deepdup
+ dup.payload = @payload.deepdup
+ else
+ dup.payload = @payload.dup
+ end
+ return dup
+ end
+
+ def flow_id
+ raise NotImplementedError
+ end
+
+ # Reassemble, reorder, and merge packets.
+ def self.normalize packets
+ begin
+ packets = TCP.reorder packets
+ rescue TCP::ReorderError => e
+ Pcap.warning e
+ end
+
+ begin
+ packets = SCTP.reorder packets
+ rescue SCTP::ReorderError => e
+ Pcap.warning e
+ end
+
+ begin
+ packets = TCP.merge packets
+ rescue TCP::MergeError => e
+ Pcap.warning e
+ end
+ return packets
+ end
+
+ # Remove non-L7/DNS/DHCP traffic if there is L7 traffic. Returns
+ # original packets if there is no L7 traffic.
+ IGNORE_UDP_PORTS = [
+ 53, # DNS
+ 67, 68, # DHCP
+ 546, 547 # DHCPv6
+ ]
+ def self.isolate_l7 packets
+ cleaned_packets = []
+ packets.each do |packet|
+ if TCP.tcp? packet
+ cleaned_packets << packet
+ elsif UDP.udp? packet
+ src_port = packet.payload.payload.src_port
+ dst_port = packet.payload.payload.dst_port
+ if not IGNORE_UDP_PORTS.member? src_port and
+ not IGNORE_UDP_PORTS.member? dst_port
+ cleaned_packets << packet
+ end
+ elsif SCTP.sctp? packet
+ cleaned_packets << packet
+ end
+ end
+ if cleaned_packets.empty?
+ return packets
+ end
+ return cleaned_packets
+ end
+
+ def to_bytes
+ io = StringIO.new
+ write io
+ io.close
+ return io.string
+ end
+
+ def == other
+ return self.class == other.class && self.payload == other.payload &&
+ self.payload_raw == other.payload_raw
+ end
+end
+
+end
+end
155 lib/mu/pcap/pkthdr.rb
@@ -0,0 +1,155 @@
+# http://www.mudynamics.com
+# http://labs.mudynamics.com
+# http://www.pcapr.net
+
+module Mu
+class Pcap
+
+class Pkthdr
+ attr_accessor :endian, :ts_sec, :ts_usec, :caplen, :len, :pkt, :pkt_raw
+
+ def initialize endian=BIG_ENDIAN, ts_sec=0, ts_usec=0, caplen=0, len=0, pkt=nil
+ @endian = endian
+ @ts_sec = ts_sec
+ @ts_usec = ts_usec
+ @caplen = caplen
+ @len = len
+ @pkt = pkt
+ @pkt_raw = pkt
+ end
+
+ FMT_NNNN = 'NNNN'
+ FMT_VVVV = 'VVVV'
+ def self.read io, endian=BIG_ENDIAN
+ if endian == BIG_ENDIAN
+ format = FMT_NNNN
+ elsif endian == LITTLE_ENDIAN
+ format = FMT_VVVV
+ end
+ bytes = io.read 16
+ if not bytes
+ raise EOFError, 'Missing PCAP packet header'
+ end
+ if not bytes.length == 16
+ raise ParseError, "Truncated PCAP packet header: expected 16 bytes, got #{bytes.length} bytes"
+ end
+ ts_sec, ts_usec, caplen, len = bytes.unpack format
+ pkt = io.read(caplen)
+ if not pkt
+ raise ParseError, 'Missing PCAP packet header packet'
+ end
+ if not pkt.length == caplen
+ raise ParseError, "Truncated PCAP packet header: expected #{pkthdr.caplen} bytes, got #{pkthdr.pkt.length} bytes"
+ end
+ pkthdr = Pkthdr.new endian, ts_sec, ts_usec, caplen, len, pkt
+ return pkthdr
+ end
+
+ def write io
+ if @pkt.is_a? String
+ pkt = @pkt
+ else
+ string_io = StringIO.new
+ @pkt.write string_io
+ pkt = string_io.string
+ end
+ len = pkt.length
+ bytes = [@ts_sec, @ts_usec, len, len].pack FMT_NNNN
+ io.write bytes
+ io.write pkt
+ end
+
+ def decode! endian, linktype
+ @pkt = case linktype
+ when DLT_NULL; Pkthdr.decode_null endian, @pkt
+ when DLT_EN10MB; Pkthdr.decode_en10mb @pkt
+ when DLT_RAW; raise NotImplementedError
+ when DLT_LINUX_SLL; Pkthdr.decode_linux_sll @pkt
+ else raise ParseError, "Unknown PCAP linktype: #{linktype}"
+ end
+ end
+
+ # See http://wiki.wireshark.org/NullLoopback
+ # and epan/aftypes.h in wireshark code.
+ BSD_AF_INET6 = [
+ OPENBSD_AF_INET6 = 24,
+ FREEBSD_AF_INET6 = 28,
+ DARWIN_AF_INET6 = 30
+ ]
+
+ def self.decode_null endian, bytes
+ Pcap.assert bytes.length >= 4, 'Truncated PCAP packet header: ' +
+ "expected at least 4 bytes, got #{bytes.length} bytes"
+ if endian == BIG_ENDIAN
+ format = 'N'
+ elsif endian == LITTLE_ENDIAN
+ format = 'V'
+ end
+ family = bytes[0, 4].unpack(format)[0]
+ bytes = bytes[4..-1]
+ ethernet = Ethernet.new
+ ethernet.src = '00:01:01:00:00:01'
+ ethernet.dst = '00:01:01:00:00:02'
+ ethernet.payload = ethernet.payload_raw = bytes
+ if family != Socket::AF_INET and family != Socket::AF_INET6 and
+ not BSD_AF_INET6.include?(family)
+ raise ParseError, "Unknown PCAP packet header family: #{family}"
+ end
+ begin
+ case family
+ when Socket::AF_INET
+ ethernet.type = Ethernet::ETHERTYPE_IP
+ ethernet.payload = IPv4.from_bytes ethernet.payload
+ when Socket::AF_INET6, FREEBSD_AF_INET6, OPENBSD_AF_INET6, DARWIN_AF_INET6
+ ethernet.type = Ethernet::ETHERTYPE_IP6
+ ethernet.payload = IPv6.from_bytes ethernet.payload
+ else
+ raise NotImplementedError
+ end
+ rescue ParseError => e
+ Pcap.warning e
+ end
+ return ethernet
+ end
+
+ def self.decode_en10mb bytes
+ return Ethernet.from_bytes(bytes)
+ end
+
+ def self.decode_linux_sll bytes
+ Pcap.assert bytes.length >= 16, 'Truncated PCAP packet header: ' +
+ "expected at least 16 bytes, got #{bytes.length} bytes"
+ packet_type, link_type, addr_len = bytes.unpack('nnn')
+ type = bytes[14, 2].unpack('n')[0]
+ bytes = bytes[16..-1]
+ ethernet = Ethernet.new
+ ethernet.type = type
+ ethernet.src = '00:01:01:00:00:01'
+ ethernet.dst = '00:01:01:00:00:02'
+ ethernet.payload = ethernet.payload_raw = bytes
+ begin
+ case type
+ when Ethernet::ETHERTYPE_IP
+ ethernet.payload = IPv4.from_bytes ethernet.payload
+ when Ethernet::ETHERTYPE_IP6
+ ethernet.payload = IPv6.from_bytes ethernet.payload
+ end
+ rescue ParseError => e
+ Pcap.warning e
+ end
+ return ethernet
+ end
+
+ def == other
+ return self.class == other.class &&
+ self.endian == other.endian &&
+ self.ts_sec == other.ts_sec &&
+ self.ts_usec == other.ts_usec &&
+ self.caplen == other.caplen &&
+ self.len == other.len &&
+ self.pkt == other.pkt
+ end
+end
+
+end
+end
61 lib/mu/pcap/reader.rb
@@ -0,0 +1,61 @@
+# http://www.mudynamics.com
+# http://labs.mudynamics.com
+# http://www.pcapr.net
+
+module Mu
+class Pcap
+
+class Reader
+ attr_accessor :pcap2scenario
+
+ FAMILY_TO_READER = {}
+
+ # Returns a reader instance of specified family. Returns nil when family is :none.
+ def self.reader family
+ if family == :none
+ return nil
+ end
+
+ if klass = FAMILY_TO_READER[family]
+ return klass.new
+ end
+
+ raise ArgumentError, "Unknown protocol family: '#{family}'"
+ end
+
+ # Returns family name
+ def family
+ raise NotImplementedError
+ end
+
+ # Notify parser of bytes written. Parser may update state
+ # to serve as a hint for subsequent reads.
+ def record_write bytes, state=nil
+ begin
+ do_record_write bytes, state
+ rescue
+ nil
+ end
+ end
+
+ # Returns next complete message from byte stream or nil.
+ def read_message bytes, state=nil
+ read_message! bytes.dup, state
+ end
+
+ # Mutating form of read_message. Removes a complete message
+ # from input stream. Returns the message or nil if there.
+ # is not a complete message.
+ def read_message! bytes, state=nil
+ begin
+ do_read_message! bytes, state
+ rescue
+ nil
+ end
+ end
+
+end
+end
+end
+
+require 'mu/pcap/reader/http_family'
170 lib/mu/pcap/reader/http_family.rb
@@ -0,0 +1,170 @@
+# http://www.mudynamics.com
+# http://labs.mudynamics.com
+# http://www.pcapr.net
+
+require 'mu/pcap/reader'
+require 'stringio'
+require 'zlib'
+
+module Mu
+class Pcap
+class Reader
+
+# Reader for HTTP family of protocols (HTTP/SIP/RTSP).
+# Handles message boundaries and decompressing/dechunking payloads.
+class HttpFamily < Reader
+ FAMILY = :http
+ FAMILY_TO_READER[FAMILY] = self
+ CRLF = "\r\n"
+ def family
+ FAMILY
+ end
+
+ def do_record_write bytes, state=nil
+ return if not state
+ if bytes =~ RE_REQUEST_LINE
+ method = $1
+ requests = state[:requests] ||= []
+ requests << method
+ end
+ end
+ private :do_record_write
+
+ RE_CONTENT_ENCODING = /^content-encoding:\s*(gzip|deflate)/i
+ RE_CHUNKED = /Transfer-Encoding:\s*chunked/i
+ RE_HEADERS_COMPLETE = /.*?\r\n\r\n/m
+ # Request line e.g. GET /index.html HTTP/1.1
+ RE_REQUEST_LINE = /\A([^ \t\r\n]+)[ \t]+([^ \t\r\n]+)[ \t]+(HTTP|SIP|RTSP)\/[\d.]+.*\r\n/
+ # Status line e.g. SIP/2.0 404 Authorization required
+ RE_STATUS_LINE = /\A((HTTP|SIP|RTSP)\/[\d.]+[ \t]+(\d+))\b.*\r\n/
+
+ RE_CONTENT_LENGTH = /^(Content-Length)(:\s*)(\d+)\r\n/i
+ RE_CONTENT_LENGTH_SIP = /^(Content-Length|l)(:\s*)(\d+)\r\n/i
+
+
+ def do_read_message! bytes, state=nil
+ case bytes
+ when RE_REQUEST_LINE
+ proto = $3
+ when RE_STATUS_LINE
+ proto = $2
+ status = $3.to_i
+ if state
+ requests = state[:requests] ||= []
+ if requests[0] == "HEAD"
+ reply_to_head = true
+ end
+ if status > 199
+ # We have a final response. Forget about request.
+ requests.shift
+ end
+ end
+ else
+ return nil # Not http family.
+ end
+
+ # Read headers
+ if bytes =~ RE_HEADERS_COMPLETE
+ headers = $&
+ rest = $'
+ else
+ return nil
+ end
+ message = [headers]
+
+ # Read payload.
+ if proto == 'SIP'
+ re_content_length = RE_CONTENT_LENGTH_SIP
+ else
+ re_content_length = RE_CONTENT_LENGTH
+ end
+ if reply_to_head
+ length = 0
+ elsif headers =~ RE_CHUNKED
+ # Read chunks, dechunking in runtime case.
+ raw, dechunked = get_chunks(rest)
+ if raw
+ length = raw.length
+ payload = raw
+ else
+ return nil # Last chunk not received.
+ end
+ elsif headers =~ re_content_length
+ length = $3.to_i
+ if rest.length >= length
+ payload = rest.slice(0,length)
+ else
+ return nil # Not enough bytes.
+ end
+ else
+ # XXX. When there is a payload and no content-length
+ # header HTTP RFC says to read until connection close.
+ length = 0
+ end
+
+ message << payload
+
+ # Consume message from input bytes.
+ message_len = headers.length + length
+ if bytes.length >= message_len
+ bytes.slice!(0, message_len)
+ return message.join
+ else
+ return nil # Not enough bytes.
+ end
+ end
+ private :do_read_message!
+
+ # Returns array containing raw and dechunked payload. Returns nil
+ # if payload cannot be completely read.
+ RE_CHUNK_SIZE_LINE = /\A([[:xdigit:]]+)\r\n?/
+ def get_chunks bytes
+ raw = []
+ dechunked = []
+ io = StringIO.new bytes
+ until io.eof?
+ # Read size line
+ size_line = io.readline
+ raw << size_line
+ if size_line =~ RE_CHUNK_SIZE_LINE
+ chunk_size = $1.to_i(16)
+ else
+ # Malformed chunk size line
+ $stderr.puts "malformed size line : #{size_line.inspect}"
+ return nil
+ end
+
+ # Read chunk data
+ chunk = io.read(chunk_size)
+ if chunk.size < chunk_size
+ # malformed/incomplete
+ $stderr.puts "malformed/incomplete #{chunk_size}"
+ return nil
+ end
+ raw << chunk
+ dechunked << chunk
+ # Get end-of-chunk CRLF
+ crlf = io.read(2)
+ if crlf == CRLF
+ raw << crlf
+ else
+ # CRLF has not arrived or, if this is the last chunk,
+ # we might be looking at the first two bytes of a trailer
+ # and we don't support trailers (see rfc 2616 sec3.6.1).
+ return nil
+ end
+
+ if chunk_size == 0
+ # Done. Return raw and dechunked payloads.
+ return raw.join, dechunked.join
+ end
+ end
+
+ # EOF w/out reaching last chunk.
+ return nil
+ end
+end
+
+end
+end
+end
367 lib/mu/pcap/sctp.rb
@@ -0,0 +1,367 @@
+# http://www.mudynamics.com
+# http://labs.mudynamics.com
+# http://www.pcapr.net
+
+module Mu
+class Pcap
+
+class SCTP < Packet
+ attr_accessor :src_port, :dst_port, :verify_tag, :checksum
+
+ # SCTP chunk types
+ CHUNK_DATA = 0x00
+ CHUNK_INIT = 0x01
+ CHUNK_INIT_ACK = 0x02
+ CHUNK_SACK = 0x03
+ CHUNK_HEARTBEAT = 0x04
+ CHUNK_HEARTBEAT_ACK = 0x05
+ CHUNK_ABORT = 0x06
+ CHUNK_SHUTDOWN = 0x07
+ CHUNK_SHUTDOWN_ACK = 0x08
+ CHUNK_ERROR = 0x09
+ CHUNK_COOKIE_ECHO = 0x0A
+ CHUNK_COOKIE_ACK = 0x0B
+ CHUNK_ECNE = 0x0C
+ CHUNK_CWR = 0x0D
+ CHUNK_SHUTDOWN_COMPLETE = 0x0E
+ CHUNK_AUTH = 0x0F
+ CHUNK_ASCONF_ACK = 0x80
+ CHUNK_PADDING = 0x84
+ CHUNK_FORWARD_TSN = 0xC0
+ CHUNK_ASCONF = 0xC1
+
+ # SCTP parameter types
+ PARAM_IPV4 = 0x0005
+ PARAM_IPV6 = 0x0006
+ PARAM_STATE_COOKIE = 0x0007
+ PARAM_COOKIE_PRESERVATIVE = 0x0009
+ PARAM_HOST_NAME_ADDR = 0x000B
+ PARAM_SUPPORTED_ADDR_TYPES = 0x000C
+ PARAM_ECN = 0x8000
+ PARAM_RANDOM = 0x8002
+ PARAM_CHUNK_LIST = 0x8003
+ PARAM_HMAC_ALGORITHM = 0x8004
+ PARAM_PADDING = 0x8005
+ PARAM_SUPPORTED_EXTENSIONS = 0x8006
+ PARAM_FORWARD_TSN = 0xC000
+ PARAM_SET_PRIMARY_ADDR = 0xC004
+ PARAM_ADAPTATION_LAYER_INDICATION = 0xC006
+
+ def initialize
+ super
+
+ @src_port = 0
+ @dst_port = 0
+ @verify_tag = 0
+ @checksum = 0
+ @payload = []
+ end
+
+ def flow_id
+ return [:sctp, @src_port, @dst_port, @verify_tag]
+ end
+
+ def reverse_flow_id
+ return [:sctp, @dst_port, @src_port, @checksum]
+ end
+
+ # Creates SCTP packet from the payload
+ def self.from_bytes bytes
+ # Basic packet validation
+ Pcap.assert(bytes.length >= 12,
+ "Truncated SCTP header: 12 > #{bytes.length}")
+ Pcap.assert(bytes.length >= 16,
+ "Truncated SCTP packet: got only #{bytes.length} bytes")
+
+ # Read SCTP header
+ sport, dport, vtag, cksum = bytes.unpack('nnNN')
+
+ # Create SCTP packet and populate SCTP header fields
+ sctp = SCTP.new
+ sctp.src_port = sport
+ sctp.dst_port = dport
+ sctp.verify_tag = vtag
+ sctp.checksum = cksum
+
+ # Initialize the counter
+ length = 12
+
+ # Collect the chunks
+ while length < bytes.length
+ # Parse new chunk from the bytes
+ chunk = Chunk.from_bytes(bytes[length..-1])
+
+ # Get chunk size with padding
+ length += chunk.padded_size
+
+ # Add chunk to the list
+ sctp << chunk
+ end
+
+ # Sync the payload
+ sctp.sync_payload
+
+ # Return the result
+ return sctp
+ end
+
+ class ReorderError < StandardError ; end
+
+ # Reorders SCTP packets, if necessary
+ def self.reorder packets
+ # Initialize
+ tsns = {}
+ init_packets = {}
+ init_ack_packets = {}
+ reordered_packets = []
+
+ # Iterate over each packet
+ while not packets.empty?
+ # Get next packet
+ packet = packets.shift
+
+ # Do not reorder non-SCTP packets
+ if not sctp?(packet)
+ reordered_packets << packet
+ else
+ # Get SCTP portion
+ sctp = packet.payload.payload
+
+ # Sanity checks and packet filtering/preprocessing
+ if 0 == sctp.verify_tag and not sctp.init?
+ # Non-Init packet with 0 verify tag
+ raise ReorderError, "Non-Init packet with zero verify tag"
+ elsif sctp.init_or_ack? and 1 < sctp.chunk_count
+ # Init/InitAck packet with more with one chunk
+ raise ReorderError, "Init/Ack packet with more than 1 chunk"
+ elsif sctp.init?
+ # Use checksum to save reverse verify tag in the Init packet
+ sctp.checksum = sctp[0].init_tag
+
+ # Save orphaned Init packets until we find the Ack
+ init_packets[sctp.reverse_flow_id] = sctp
+
+ # Add packet for further processing
+ reordered_packets << packet
+ elsif sctp.init_ack?
+ # Lookup Init packet and construct it's flow it
+ init_packet = init_packets.delete(sctp.flow_id)
+
+ # Did we find anything?
+ if init_packet
+ # Set verify tag in the Init packet
+ init_packet.verify_tag = sctp[0].init_tag
+
+ # Set reverse verify tag in the InitAck packet
+ sctp.checksum = init_packet.verify_tag
+
+ # Re-insert INIT packet keyed by its flow id
+ init_packets[init_packet.flow_id] = init_packet
+ else
+ Pcap.warning("Orphaned SCTP INIT_ACK packet")
+ end
+
+ # Save InitAck packet
+ init_ack_packets[sctp.flow_id] = sctp
+
+ # Add packet for further processing
+ reordered_packets << packet
+ elsif sctp.has_data?
+ # SCTP packet with user data; lookup Init or InitAck packet
+ init_packet = init_packets[sctp.flow_id]
+ init_ack_packet = init_ack_packets[sctp.flow_id]
+
+ # It should belong to either one flow id or the other
+ if init_packet
+ # Set reverse verify tag from Init packet
+ sctp.checksum = init_packet.checksum
+ elsif init_ack_packet
+ # Set reverse flow id from InitAck packet
+ sctp.checksum = init_ack_packet.checksum
+ else
+ # Orphaned SCTP packet -- not very good
+ Pcap.warning("Orphaned SCTP DATA packet detected")
+ end
+
+ # If we have just one chunk we are done
+ if 1 == sctp.chunk_count and not tsns.member?(sctp[0].tsn)
+ # Save TSN
+ tsns[sctp[0].tsn] = sctp[0]
+
+ # sync the payload
+ sctp.sync_payload
+
+ # Add packet for further processing
+ reordered_packets << packet
+ else
+ # Split each data chunk in a separate SCTP packet
+ sctp.chunk_count.times do
+ # Get next chunk
+ chunk = sctp.shift
+
+ # Is it data?
+ if CHUNK_DATA == chunk.type
+ # Yes, check for duplicate TSNs
+ if not tsns.member?(chunk.tsn)
+ # Not a duplicate; create new SCTP packet
+ packet_new = packet.deepdup
+
+ # Create new SCTP payload
+ sctp_new = SCTP.new
+ sctp_new.src_port = sctp.src_port
+ sctp_new.dst_port = sctp.dst_port
+ sctp_new.verify_tag = sctp.verify_tag
+ sctp_new.checksum = sctp.checksum
+
+ # Add the chunk
+ sctp_new << chunk
+
+ # Add SCTP payload to the new packet
+ packet_new.payload.payload = sctp_new
+
+ # Save TSN
+ tsns[chunk.tsn] = chunk
+
+ # Sync the payload
+ sctp_new.sync_payload
+
+ # Add packet for further processing
+ reordered_packets << packet_new
+ else
+ Pcap.warning("Duplicate chunk: #{chunk.tsn}")
+ end
+ else
+ Pcap.warning("Non-data chunk: #{chunk.type}")
+ end
+ end
+ end
+ else
+ # Other SCTP packet; we are not interested at this time
+ end
+ end
+ end
+
+ # Return the result
+ return reordered_packets
+ end
+
+ def write io, ip
+ # Give a warning if packet size exceeds maximum allowed
+ if @payload_raw and @payload_raw.length + 20 > 65535
+ Pcap.warning("SCTP payload is too large")
+ end
+
+ # Calculate CRC32 checksum on the packet; temporarily removed due to a
+ # hack that uses checksum to link forward and reverse SCTP flow IDs.
+ #header = [@src_port, @dst_port, @verify_tag, 0].pack('nnNN')
+ #checksum = SCTP.crc32(header + @payload_raw)
+ header = [@src_port, @dst_port, @verify_tag, @checksum].pack('nnNN')
+
+ # Write SCTP header followed by each chunk
+ io.write(header)
+
+ # Write each chunks' data
+ @payload.each do |chunk|
+ chunk.write(io, ip)
+ end
+ end
+
+ def sync_payload
+ # Reset raw bytes
+ @payload_raw = ''
+
+ # Iterate over each chunk
+ @payload.each do |chunk|
+ @payload_raw << chunk.payload_raw
+ end
+
+ # Reset raw payload if it's empty
+ @payload_raw = nil if @payload_raw == ''
+ end
+
+ def self.crc32 bytes
+ r = 0xFFFFFFFF
+
+ bytes.each_byte do |b|
+ r ^= b
+
+ 8.times do
+ r = (r >> 1) ^ (0xEDB88320 * (r & 1))
+ end
+ end
+
+ return r ^ 0xFFFFFFFF
+ end
+
+ def self.sctp? packet
+ return packet.is_a?(Ethernet) &&
+ packet.payload.is_a?(IP) &&
+ packet.payload.payload.is_a?(SCTP)
+ end
+
+ def << chunk
+ @payload << chunk
+ end
+
+ def shift
+ return @payload.shift
+ end
+
+ def [] index
+ return @payload[index]
+ end
+
+ def chunk_count
+ return @payload.size
+ end
+
+ def has_data?
+ return @payload.any? do |chunk|
+ CHUNK_DATA == chunk.type
+ end
+ end
+
+ def to_s
+ return "sctp(%d, %d, %d, %s)" % [@src_port,
+ @dst_port,
+ @verify_tag,
+ @payload.join(", ")]
+ end
+
+ def == other
+ return super &&
+ self.src_port == other.src_port &&
+ self.dst_port == other.dst_port &&
+ self.verify_tag == other.verify_tag &&
+ self.payload.size == other.payload.size
+ end
+
+ def init?
+ if CHUNK_INIT == @payload[0].type
+ return true
+ else
+ return false
+ end
+ end
+
+ def init_ack?
+ if CHUNK_INIT_ACK == @payload[0].type
+ return true
+ else
+ return false
+ end
+ end
+
+ def init_or_ack?
+ if CHUNK_INIT == @payload[0].type or CHUNK_INIT_ACK == @payload[0].type
+ return true
+ else
+ return false
+ end
+ end
+end # class SCTP
+
+end # class Pcap
+end # module Mu
+
+require 'mu/pcap/sctp/chunk'
123 lib/mu/pcap/sctp/chunk.rb
@@ -0,0 +1,123 @@
+# http://www.mudynamics.com
+# http://labs.mudynamics.com
+# http://www.pcapr.net
+
+module Mu
+class Pcap
+class SCTP
+
+class Chunk < Packet