scriptroute 0.4.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,129 @@
1
+ module Scriptroute
2
+ class LivenessTest
3
+ # whether a host is responsive for measurement; try_icmp or try_tcp.
4
+
5
+ attr_reader :responsives
6
+ attr_reader :pingfiltering
7
+ attr_reader :broken # do ping, but not http.
8
+ attr_reader :unresponsives
9
+ def LivenessTest.try_icmp(destinations)
10
+ retvalResponsives = []
11
+ retvalUnresponsives = []
12
+ begin
13
+ responses = Scriptroute::send_train( destinations.map { |dst|
14
+ begin
15
+ probe = Scriptroute::ICMPecho.new
16
+ probe.ip_dst = dst
17
+ Struct::DelayedPacket.new(0.01,probe)
18
+ rescue RuntimeError => e
19
+ # happens if dns lookup doesn't work; we'll mark it dead.
20
+ # 2013-aug added to handle dns fail.
21
+ retvalUnresponsives.push(dst)
22
+ nil
23
+ rescue => e
24
+ # happens if dns lookup doesn't work; we'll mark it dead.
25
+ retvalUnresponsives.push(dst)
26
+ nil
27
+ end
28
+ }.compact )
29
+ responses.each_with_index { |tuple,i|
30
+ if(tuple.response &&
31
+ tuple.response.packet.is_a?(Scriptroute::ICMP) &&
32
+ tuple.response.packet.icmp_type == Scriptroute::ICMP::ICMP_ECHOREPLY) then
33
+ retvalResponsives.push(destinations[i])
34
+ else
35
+ retvalUnresponsives.push(destinations[i])
36
+ end
37
+ }
38
+ rescue => e
39
+ if(e=~/packet (%d) of %d/) then
40
+ puts "LivenessTest.try_icmp(#{destinations.join(', ')}) took #{e} when
41
+ contacting %s (or maybe %s)" % [ destinations[$1.to_i], destinations[$1.to_i-1] ]
42
+ else
43
+ puts "LivenessTest.try_icmp(#{destinations.join(', ')}) took #{e}"
44
+ end
45
+ end
46
+ return retvalResponsives, retvalUnresponsives;
47
+ end
48
+
49
+ def LivenessTest.try_tcp(destinations, use_syn=false)
50
+ retvalResponsives = []
51
+ retvalUnresponsives = []
52
+ if (destinations.length < 1) then
53
+ raise "you want to give a list of destinations to probe"
54
+ end
55
+ begin
56
+ responses = Scriptroute::send_train( destinations.map { |dst|
57
+ probe = Scriptroute::TCP.new(0)
58
+ probe.ip_dst = dst
59
+ probe.flag_syn = use_syn
60
+ probe.flag_ack = !use_syn
61
+ if(use_syn) then
62
+ probe.th_dport = 80
63
+ end
64
+ Struct::DelayedPacket.new(0.01,probe)
65
+ } )
66
+ responses.each_with_index { |tuple,i|
67
+ puts tuple.probe.inspect
68
+ puts (tuple.response.inspect or "no response")
69
+ # puts tuple.to_s
70
+ if(tuple.response && tuple.response.packet.is_a?(Scriptroute::TCP) &&
71
+ ( (use_syn && tuple.response.packet.flag_syn) ||
72
+ (!use_syn && tuple.response.packet.flag_rst)) ) then
73
+ retvalResponsives.push(destinations[i])
74
+ else
75
+ retvalUnresponsives.push(destinations[i])
76
+ end
77
+ }
78
+ rescue => e
79
+ if(e.to_s =~ /packet (\d+) of \d+/) then
80
+ puts "LivenessTest.try_tcp(...) took #{e} (#{$1} is to #{destinations[$1.to_i]} maybe #{destinations[$1.to_i - 1]} "
81
+ else
82
+ puts "LivenessTest.try_tcp(#{destinations.join(', ')}) took #{e} #{e.backtrace[0]}"
83
+ end
84
+ return destinations, [];
85
+ end
86
+ return retvalResponsives, retvalUnresponsives;
87
+ end
88
+
89
+ def initialize(destinations)
90
+ @responsives = []
91
+ @unresponsives = []
92
+ @broken = []
93
+ if(destinations.is_a?(String)) then
94
+ # promote to an array
95
+ destinations = [ destinations ]
96
+ end
97
+ if(destinations.length == 0) then
98
+ raise "Empty array passed to LivenessTest.new()"
99
+ end
100
+ icmp_responsive, icmp_unresponsive = LivenessTest.try_icmp(destinations);
101
+ @pingfiltering, @unresponsives = if(icmp_unresponsive.length > 0) then
102
+ LivenessTest.try_tcp(icmp_unresponsive, false);
103
+ else
104
+ [[], [] ]
105
+ end
106
+ @responsives, @broken = if(icmp_responsive.length > 0) then
107
+ LivenessTest.try_tcp(icmp_responsive, true);
108
+ else
109
+ [[], []]
110
+ end
111
+ end
112
+
113
+ def to_s
114
+ "Responsives: " + @responsives.join(" ") +
115
+ "\nUnresponsivees: " + @unresponsives.join(" ") +
116
+ "\nPing Filtering: " + @pingfiltering.join(" ") +
117
+ "\nBroken (ping not http): " + @broken.join(" ")
118
+ end
119
+ def summary
120
+ "%d responsive, %d unresponsive" % [ @responsives.length, @unresponsives.length ]
121
+ end
122
+ end
123
+ end
124
+
125
+ if __FILE__ == $0
126
+ if ARGV.length > 0 then
127
+ puts LivenessTest.new(ARGV)
128
+ end
129
+ end
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/ruby
2
+ # generates a pretty version of an ip address using
3
+ # a reverse dns lookup and running the name through
4
+ # undns for decoding.
5
+
6
+ # one might modify this to consult other databases
7
+ # in the process.
8
+
9
+ require 'scriptroute'
10
+
11
+ $have_undns = begin
12
+ require 'undns'
13
+ true
14
+ rescue LoadError, SecurityError
15
+ false
16
+ end
17
+ $have_origins = begin
18
+ $have_undns and Undns.origins_init
19
+ true
20
+ rescue
21
+ false
22
+ end
23
+ $have_socket = begin
24
+ require 'socket'
25
+ true
26
+ rescue LoadError, SecurityError
27
+ $stderr.puts "couldn't load socket"
28
+ false
29
+ end
30
+ $have_resolv = if(!$have_socket or Socket.gethostbyname("127.0.0.1")[0] !~ /localhost/) then
31
+ begin
32
+ # if socket library is not there or doesn't effectively do reverse lookups (dammit)
33
+ require 'resolv'
34
+ true
35
+ rescue LoadError, SecurityError
36
+ $stderr.puts "couldn't load resolv"
37
+ false
38
+ end
39
+ else
40
+ false # don't need it.
41
+ end
42
+
43
+ module Scriptroute
44
+ def Scriptroute::nameify(addr)
45
+ unless addr.is_a?(Scriptroute::IPaddress) then
46
+ # if not a known IP address, validate.
47
+ if addr == nil or addr == '' then
48
+ # empty string. usually a mistake somewhere.
49
+ $stderr.puts "nameify('') called from:\n " + Kernel.caller.join("\n ")
50
+ return addr
51
+ end
52
+ if(! /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(addr)) then
53
+ # not an ip address looking thing.
54
+ $stderr.puts "nameify(#{addr}) called from:\n " + Kernel.caller.join("\n ")
55
+ return addr
56
+ end
57
+ end
58
+ name =
59
+ if($have_resolv) then
60
+ begin
61
+ Resolv.getnames(addr.to_s)[0]
62
+ rescue ArgumentError => boom
63
+ # seems to happen for RFC1918 space, but probably just for those
64
+ # addresses without a name.
65
+ $stderr.puts "argument error resolving '#{addr}' to name using resolv: " + boom
66
+ nil
67
+ rescue SocketError => boom
68
+ nil
69
+ end
70
+ elsif($have_socket) then
71
+ begin
72
+ name = (Socket.gethostbyname(addr.to_s)[0]).gsub(/\"/,'')
73
+ name
74
+ rescue ArgumentError => boom
75
+ # seems to happen for RFC1918 space, but probably just for those
76
+ # addresses without a name.
77
+ $stderr.puts "argument error resolving '#{addr}' to name: " + boom
78
+ nil
79
+ rescue SocketError => boom
80
+ nil
81
+ end
82
+ else
83
+ nil
84
+ end
85
+ attributes = if($have_undns) then
86
+ if(name) then
87
+ asn = Undns.get_asn_bydns(name)
88
+ if($have_origins) then
89
+ " [%d/%d]" % [ asn, Undns.origin_for_address_str(addr) ]
90
+ elsif(asn > 0) then
91
+ " [%d]" % [ asn ]
92
+ else
93
+ ""
94
+ end +
95
+ if(asn > 0) then
96
+ " {%s}" % [ Undns.get_loc(asn, name) ]
97
+ else
98
+ ""
99
+ end
100
+ elsif($have_origins) then
101
+ # don't have a name.
102
+ " [/%d]" % [ Undns.origin_for_address_str(addr) ]
103
+ else
104
+ ""
105
+ end
106
+ else
107
+ ""
108
+ end
109
+
110
+ if(name) then
111
+ name + ' (' + addr.to_s + ')' + attributes
112
+ else
113
+ addr.to_s + attributes
114
+ end
115
+ end
116
+ end
117
+
118
+
119
+ if $0 == __FILE__ then
120
+ # Undns.want_debug=1
121
+ # Undns.want_conventions_debug=1 seems to fail to work for some reason.
122
+ puts nameify("12.123.203.170")
123
+ puts Undns.get_loc(7018, "tbr1-p012502.st6wa.ip.att.net")
124
+ puts Undns.get_loc(0, "tbr1-p012502.st6wa.ip.att.net")
125
+ puts Undns.get_asn_bydns("tbr1-p012502.st6wa.ip.att.net")
126
+ end
127
+
@@ -0,0 +1,800 @@
1
+ module Scriptroute
2
+ # a small library of routines for constructing packets
3
+ # as strings to hand off to the interpreter.
4
+
5
+ class IPv4
6
+ IPPROTO_ICMP = 1
7
+ IPPROTO_TCP = 6
8
+ IPPROTO_UDP = 17
9
+ end
10
+
11
+ class ICMP < IPv4
12
+ ICMP_ECHO = 8
13
+ ICMP_ECHOREPLY = 0
14
+ ICMP_UNREACH = 3
15
+ ICMP_TIMXCEED = 11
16
+ ICMP_TIMESTAMP = 13
17
+ ICMP_TIMESTAMPREPLY = 14
18
+ ICMP_MASKREQ = 17 # /* address mask request */
19
+ ICMP_MASKREPLY = 18 # /* address mask reply */
20
+ ICMP_PARAMETERPROB = 12
21
+
22
+ ICMP_UNREACH_NET = 0 # /* bad net */
23
+ ICMP_UNREACH_HOST = 1 # /* bad host */
24
+ ICMP_UNREACH_PROTOCOL = 2 # /* bad protocol */
25
+ ICMP_UNREACH_PORT = 3 # /* bad port */
26
+ ICMP_UNREACH_NEEDFRAG = 4 # /* IP_DF caused drop */
27
+ ICMP_UNREACH_SRCFAIL = 5 # /* src route failed */
28
+ ICMP_UNREACH_NET_UNKNOWN = 6 # /* unknown net */
29
+ ICMP_UNREACH_HOST_UNKNOWN = 7 # /* unknown host */
30
+ ICMP_UNREACH_ISOLATED = 8 # /* src host isolated */
31
+ ICMP_UNREACH_NET_PROHIB = 9 # /* net denied */
32
+ ICMP_UNREACH_HOST_PROHIB = 10 # /* host denied */
33
+ ICMP_UNREACH_TOSNET = 11 # /* bad tos for net */
34
+ ICMP_UNREACH_TOSHOST = 12 # /* bad tos for host */
35
+ ICMP_UNREACH_FILTER_PROHIB = 13 # /* admin prohib */
36
+ ICMP_UNREACH_HOST_PRECEDENCE = 14 # /* host prec vio. */
37
+ ICMP_UNREACH_PRECEDENCE_CUTOFF = 15 # /* prec cutoff */
38
+ end
39
+
40
+ class IPv4option
41
+ IPOPT_EOOL = 0
42
+ IPOPT_EOL = 0 # deprecated, backward compatibility.
43
+ IPOPT_NOOP = 1
44
+ IPOPT_TS = 68
45
+ IPOPT_TIMESTAMP = IPOPT_TS
46
+ IPOPT_RR = 7
47
+
48
+ IPOPT_TS_TSONLY = 0
49
+ IPOPT_TS_TSANDADDR = 1
50
+ IPOPT_TS_PRESPEC = 3
51
+ end
52
+
53
+ class TCP < IPv4
54
+ TH_FIN = 0x01
55
+ TH_SYN = 0x02
56
+ TH_RST = 0x04
57
+ TH_PUSH = 0x08
58
+ TH_ACK = 0x10
59
+ TH_URG = 0x20
60
+ end
61
+
62
+ end
63
+
64
+ class Array
65
+ def inject(n)
66
+ each { |value| n = yield(n, value) }
67
+ n
68
+ end
69
+ # used for calculating header length.
70
+ def sum
71
+ inject(0) { |n, value| n + value }
72
+ end
73
+ def max
74
+ inject(0) { |n, value| ((n > value) ? n : value) }
75
+ end
76
+ def even_subscripts
77
+ r = Array.new
78
+ 0.step(self.length, 2) { |i| r.push(self[i]) }
79
+ r
80
+ end
81
+ def odd_subscripts
82
+ r = Array.new
83
+ 1.step(self.length, 2) { |i| r.push(self[i]) }
84
+ r
85
+ end
86
+ end
87
+
88
+ module Scriptroute
89
+ class IPaddress
90
+ # @param n [String,IPaddress,Integer] a hostname, an IP address in string form, another IPaddress or an IP address in numeric form
91
+ def initialize(n)
92
+ raise "Seems bad to initialize an IPaddress with nil." unless n
93
+ if(n.is_a?(String)) then
94
+ shf = 32
95
+ if n =~ /^[\d\.]+$/ then
96
+ @addr = n.split('.').map { |i| shf -= 8; i.to_i << shf }.sum
97
+ else
98
+ addr = Scriptroute::dns_lookup(n)
99
+ if addr then
100
+ @addr = addr.split('.').map { |i| shf -= 8; i.to_i << shf }.sum
101
+ else
102
+ $stderr.puts "unable to lookup #{n}"
103
+ @addr = 0
104
+ end
105
+ end
106
+ elsif(n.is_a?(IPaddress)) then
107
+ @addr = n.to_i
108
+ else
109
+ @addr = n
110
+ end
111
+ end
112
+ # @return [String] dotted quad notation of the IP address.
113
+ def to_s
114
+ [ @addr >> 24, @addr >> 16, @addr >> 8, @addr ].map { |i| i & 0xff }.join(".")
115
+ end
116
+ # just invokes to_s for now.
117
+ # @return [String]
118
+ def inspect
119
+ to_s
120
+ end
121
+ # @return [Integer]
122
+ def to_i
123
+ @addr
124
+ end
125
+ # in case you would like to store a {Hash} with IPaddresses as keys
126
+ def hash
127
+ @addr
128
+ end
129
+ # in case you would like to store a {Hash} with IPaddresses as keys
130
+ def eql?(other)
131
+ @addr == other.to_i
132
+ end
133
+ # Compares numerically, for sorting.
134
+ def <=>(other)
135
+ @addr <=> other.to_i
136
+ end
137
+ # Compares numerically, for equality test.
138
+ def ==(other)
139
+ @addr == other.to_i
140
+ end
141
+ # @returns [String] using {Scriptroute::nameify}
142
+ def nameify
143
+ Scriptroute.nameify(self.to_s)
144
+ end
145
+ end
146
+
147
+ class IPv4
148
+ # @return [Fixnum]
149
+ attr_reader :ip_hl, :ip_v, :ip_tos, :ip_len, :ip_id, :ip_off,
150
+ :ip_ttl, :ip_p, :ip_sum
151
+ # @return [IPaddress]
152
+ attr_reader :ip_src, :ip_dst
153
+ # @return [Array<IPv4option>]
154
+ attr_reader :ip_options
155
+ # @return [Fixnum]
156
+ attr_writer :ip_tos, :ip_id, :ip_off,
157
+ :ip_ttl, :ip_sum # :ip_dst,
158
+ # @return [Array<IPv4option>]
159
+ attr_writer :ip_options
160
+
161
+ # @param [String,IPaddress] destination_address the destination of this packet.
162
+ def ip_dst=(destination_address)
163
+ @ip_dst = IPaddress.new(destination_address)
164
+ end
165
+
166
+ # @return [String] The packet in string form
167
+ def marshal
168
+ calculate_packet_len
169
+ [ (@ip_v << 4) + @ip_hl, @ip_tos, @ip_len,
170
+ @ip_id, @ip_off,
171
+ @ip_ttl, @ip_p, @ip_sum,
172
+ @ip_src.to_i,
173
+ @ip_dst.to_i ].pack("ccn" + "nn" + "ccn" + "N" + "N") +
174
+ @ip_options.map { |o| o.marshal }.join
175
+ end
176
+ # @return [Fixnum] the length of the payload, intended to be implemented by subclasses.
177
+ # @raise [RuntimeError] if invoked directly instead of being overloaded.
178
+ def ip_payload_len
179
+ raise "ip_payload_len is a pure virtual function in IPv4"
180
+ end
181
+ # sets ip_hl, should not be necessary to call this explicitly.
182
+ # @return [void]
183
+ def calculate_header_len
184
+ @ip_hl = [ 5 + ((ip_options.map { |o| o.ipt_len }.sum)/4.0).ceil, 15 ].min # at most 15
185
+ end
186
+ # @return [void]
187
+ def calculate_packet_len
188
+ calculate_header_len # ensures @ip_hl is set properly
189
+ raise "ip_payload_len not calculated" unless ip_payload_len
190
+ @ip_len = ip_payload_len + (@ip_hl * 4)
191
+ end
192
+ # @param opt [IPv4option] add a record route or timestamp option.
193
+ # @return [void]
194
+ def add_option(opt)
195
+ opt.is_a?(IPv4option) or raise "can add only IPv4options"
196
+ @ip_options.push(opt)
197
+ calculate_header_len
198
+ end
199
+ # This method is probably only useful for calling from subclasses.
200
+ # @param p [Fixnum] the protocol, either Scriptroute::IPv4::IPPROTO_ICMP, IPPROTO_TCP, or IPPROTO_UDP
201
+ def initialize(p)
202
+ if(p.is_a?(Fixnum)) then
203
+ @ip_v = 4
204
+ @ip_tos = 0
205
+ @ip_id = 11
206
+ @ip_off = 0
207
+ @ip_ttl = 64
208
+ @ip_p = p
209
+ @ip_sum = 0
210
+ @ip_src = 0
211
+ @ip_dst = 0
212
+ @ip_options = Array.new
213
+ calculate_packet_len
214
+ else
215
+ raise "need a protocol number to instantiate an ipv4 packet"
216
+ end
217
+ end
218
+
219
+ # a by-ip_p table of packet instantiators.
220
+ @@creators = Hash.new
221
+
222
+ # invoke a factory(?) to create a TCP/UDP/ICMP as appropriate, then
223
+ # unmarshal the IPv4 header contents.
224
+ # @param [String] str the bytes of the packet to unpack.
225
+ def IPv4.creator(str)
226
+ ip_vhl, ip_tos, ip_len,
227
+ ip_id, ip_off,
228
+ ip_ttl, ip_p, ip_sum,
229
+ ip_src, ip_dst = str.unpack("ccn" + "nn" + "ccn" + "N" + "N");
230
+ ip_hl = ip_vhl & 0xf;
231
+ if(@@creators[ip_p]) then
232
+ pkt = (@@creators[ip_p]).call(str[(ip_hl * 4) .. ip_len])
233
+
234
+ pkt.ipv4_unmarshal(str)
235
+ pkt
236
+ else
237
+ raise "unknown protocol #%d in %s" % [ ip_p, str.unpack("C*").map { |c| "%x" % c }.join(' ') ]
238
+ end
239
+ end
240
+
241
+ # unpack the ipv4 component of the packet, even if we're
242
+ # unmarshaling an instance of a subclass.
243
+ # @param [String] str The string from which to unpack.
244
+ # @return [void]
245
+ def ipv4_unmarshal(str)
246
+ ip_vhl, @ip_tos, @ip_len,
247
+ @ip_id, @ip_off,
248
+ @ip_ttl, @ip_p, @ip_sum,
249
+ ip_src, ip_dst = str.unpack("CCn" + "nn" + "CCn" + "N" + "N");
250
+ @ip_src, @ip_dst = [ip_src, ip_dst].map { |addr| IPaddress.new(addr) }
251
+ @ip_hl = ip_vhl & 0xf;
252
+ @ip_v = (ip_vhl & 0xf0) >> 4;
253
+ @ip_options = Array.new
254
+ if(@ip_hl > 5) then
255
+ add_option(IPv4option.creator(str[20 .. (@ip_hl*4)]))
256
+ end
257
+ end
258
+
259
+ # @return [String]
260
+ def to_s
261
+ "%s > %s ttl%d" % [ @ip_src, @ip_dst, @ip_ttl ] +
262
+ @ip_options.map { |o| o.to_s }.join(", ")
263
+ end
264
+ private :initialize
265
+ private :marshal
266
+ end
267
+
268
+ # base class for IPv4 options.
269
+ class IPv4option
270
+ # @return [Fixnum]
271
+ attr_reader :ipt_code, :ipt_len, :ipt_ptr
272
+ attr_writer :ipt_ptr
273
+ @@creators = Hash.new
274
+ def IPv4option.creator(str)
275
+ ipt_code, ipt_len, ipt_ptr = str.unpack("CCC")
276
+ if(@@creators[ipt_code]) then
277
+ pkt = (@@creators[ipt_code]).call(str)
278
+ else
279
+ raise "unknown ip option code %d" % ipt_code
280
+ end
281
+ end
282
+ def initialize(*rest)
283
+ if(rest.length == 3) then
284
+ @ipt_code = rest[0]
285
+ @ipt_len = rest[1]
286
+ @ipt_ptr = rest[2]
287
+ else
288
+ @ipt_code, @ipt_len, @ipt_ptr = rest[0].unpack("CCC")
289
+ end
290
+ end
291
+ # @return [String] The packet in string form
292
+ def marshal
293
+ # doesn't end on a word.
294
+ [ @ipt_code, @ipt_len, @ipt_ptr ].pack("CCC")
295
+ end
296
+ # @return [String]
297
+ def to_s
298
+ ": opt: code %d len %d ptr %d" % [ @ipt_code, @ipt_len, @ipt_ptr ]
299
+ end
300
+ # must be instatiated through a derived class.
301
+ private :marshal
302
+ end
303
+
304
+ # The IPv4 EOOL option, which has a purpose.
305
+ class EndOfOptions_option < IPv4option
306
+ @@creators[IPOPT_EOOL] = lambda { |hdr|
307
+ p = EndOfOptions_option.new(hdr)
308
+ }
309
+ def initialize(flag_or_str)
310
+ if(flag_or_str.is_a?(Fixnum)) then
311
+ super(IPOPT_EOOL, 0, 1,nil) # maximum length is 40, but tcpdump whines.
312
+ else
313
+ @ipt_code = flag_or_str.unpack("c")
314
+ @ipt_len=1
315
+ end
316
+ end
317
+ # @return [String]
318
+ def to_s
319
+ ":ip_option_code=#{@ipt_code}"
320
+ end
321
+ # @return [String] the option in string form
322
+ def marshal
323
+ @ipt_code.pack("c") # oflw will be init'd to zero
324
+ end
325
+ end
326
+
327
+ # The IPv4 NOOP option, which has a purpose.
328
+ class NOOP_option < EndOfOptions_option
329
+ # an IP option that doesn't do anything, but we parse it anyway.
330
+ @@creators[IPOPT_NOOP] = lambda { |hdr| # hack in NOOP support as well
331
+ p = NOOP_option.new(hdr)
332
+ }
333
+ end
334
+
335
+ # The IPv4 timestamp option, which can determine routers
336
+ class Timestamp_option < IPv4option
337
+ # @return [Fixnum]
338
+ attr_reader :ts_flag, :ts_overflow
339
+ # @return [Array<IPaddress>,nil]
340
+ attr_reader :routers
341
+ # @return [Array<Fixnum>]
342
+ attr_reader :times
343
+ @@creators[IPOPT_TS] = lambda { |hdr|
344
+ p = Timestamp_option.new(hdr)
345
+ }
346
+ def initialize(flag_or_str)
347
+ if(flag_or_str.is_a?(Fixnum)) then
348
+ @routers = Array.new
349
+ @times = Array.new
350
+ @ts_flag = flag_or_str
351
+ super(IPOPT_TS, 36, 5) # maximum length is 40, but tcpdump whines.
352
+ else
353
+ ipt_code, ipt_len, ipt_ptr, ipt_of_fl = flag_or_str.unpack("CCCC");
354
+ @ts_flag = ipt_of_fl & 0x0f
355
+ @ts_overflow = (ipt_of_fl & 0xf0) >> 4
356
+ @routers, @times =
357
+ case @ts_flag
358
+ when IPOPT_TS_TSONLY
359
+ [ nil, flag_or_str.unpack("xxxxN*") ]
360
+ when IPOPT_TS_TSANDADDR, IPOPT_TS_PRESPEC
361
+ all = flag_or_str.unpack("xxxxN*")
362
+ [ all.even_subscripts[0...ipt_len/8].map { |rtr| IPaddress.new(rtr) },
363
+ all.odd_subscripts[0...(ipt_len/8)] ]
364
+ else
365
+ raise "bad timestamp flag: #{@ts_flag} (code: #{ipt_code}, len: #{ipt_len}, ptr: #{ipt_ptr})"
366
+ end
367
+ super( flag_or_str )
368
+ end
369
+ end
370
+ # @return [String] the option in string form
371
+ def marshal
372
+ super + [ @ts_flag ].pack("c") + # oflw will be init'd to zero
373
+ Array.new(@ipt_len - 4, 0).pack("c*")
374
+ end
375
+ end
376
+
377
+ class RecordRoute_option < IPv4option
378
+ attr_reader :routers
379
+ @@creators[IPOPT_RR] = lambda { |hdr|
380
+ p = RecordRoute_option.new(hdr)
381
+ }
382
+ def initialize(*rest)
383
+ if(rest.length == 0)
384
+ super(IPOPT_RR, 39, 4)
385
+ @routers = Array.new((@ipt_len - 3 + 1)/4, 0)
386
+ else
387
+ super(rest[0])
388
+ @routers = rest[0][3..@ipt_len].unpack("N*").map { |addr| IPaddress.new(addr) }
389
+ end
390
+ end
391
+ # @return [String] the option in string form
392
+ def marshal
393
+ super + @routers.pack("N*") + "\0"
394
+ end
395
+ # @return [String]
396
+ def to_s
397
+ super + ': RR: {' + @routers.join(", ") + '}'
398
+ end
399
+ end
400
+
401
+ class UDP < IPv4
402
+ # @return [Fixnum]
403
+ attr_reader :uh_sport, :uh_dport, :uh_ulen, :uh_sum
404
+ attr_writer :uh_dport, :uh_sum
405
+
406
+ @@creators[IPPROTO_UDP] = lambda { |hdr|
407
+ uh_sport, uh_dport, uh_ulen, uh_sum = hdr.unpack("nnnn")
408
+ if uh_sport==123 || uh_dport==123 then
409
+ p = NTP.new(hdr[8..hdr.length])
410
+ p.udp_unmarshal(hdr)
411
+ else
412
+ p = UDP.new(hdr)
413
+ end
414
+ p
415
+ }
416
+
417
+ # @return [Fixnum] the udp data plus header length, uh_ulen
418
+ def ip_payload_len
419
+ @uh_ulen
420
+ end
421
+
422
+ # Create a new UDP packet from a payload size or from contents.
423
+ # @param paylen_or_str [Integer,String] size or contents
424
+ def initialize(paylen_or_str = 0)
425
+ if(paylen_or_str.is_a?(Fixnum)) then
426
+ if( paylen_or_str < 0) then raise "payload length must be >= 0" end
427
+ @uh_ulen = paylen_or_str + 8
428
+ if(@uh_ulen > 1480) then
429
+ raise "desired packet too big"
430
+ end
431
+ @uh_sport = 32945
432
+ @uh_dport = 33434
433
+ @uh_sum = 0
434
+ super( IPPROTO_UDP )
435
+ else
436
+ @uh_sport, @uh_dport, @uh_ulen, @uh_sum = paylen_or_str.unpack("nnnn")
437
+ end
438
+ end
439
+
440
+ # @return [String] header and payload of this UDP datagram.
441
+ def marshal
442
+ # payload = "a%d"% (@payload_len)
443
+ # puts payload
444
+ if(@uh_ulen < 8) then warn "uh_ulen should be at least 8" end
445
+ array_of_elements = [ @uh_sport, @uh_dport, @uh_ulen, @uh_sum ]
446
+ raise "a UDP header field was unset" if array_of_elements.include?(nil)
447
+ super + [ @uh_sport, @uh_dport, @uh_ulen, @uh_sum ].pack("nnnn") +
448
+ if ( self.class == UDP ) then
449
+ "\0" * ( @uh_ulen - 8 )
450
+ else
451
+ "" # the subclass will take care of it
452
+ end
453
+
454
+ end
455
+
456
+ # Used for subclasses to set the UDP header fields.
457
+ # @param str [String] the udp header to parse into
458
+ # ports, length, and checksum.
459
+ # @return [void]
460
+ def udp_unmarshal(str)
461
+ @uh_sport, @uh_dport, @uh_ulen, @uh_sum = str.unpack("nnnn")
462
+ end
463
+
464
+ # @return [String]
465
+ def to_s
466
+ super + " UDP %d > %d len %d" % [ @uh_sport, @uh_dport, @uh_ulen ]
467
+ end
468
+
469
+ end
470
+
471
+ class TCP < IPv4
472
+ attr_reader :th_sport, :th_dport, :th_sum, :th_seq, :th_ack,
473
+ :th_win, :th_flags, :th_win, :th_sum, :th_urp
474
+ # flags don't work
475
+ attr_reader :flag_fin, :flag_syn, :flag_rst, :flag_push,
476
+ :flag_ack, :flag_urg
477
+ attr_writer :th_dport, :th_sum, :th_seq, :th_ack,
478
+ :th_win, :th_flags, :th_win, :th_sum
479
+ # flags don't work
480
+ attr_writer :flag_fin, :flag_syn, :flag_rst, :flag_push,
481
+ :flag_ack, :flag_urg
482
+ # @return [Fixnum]
483
+ attr_reader :ip_p, :ip_payload_len
484
+
485
+ @@creators[IPPROTO_TCP] = lambda { |hdr|
486
+ TCP.new(hdr)
487
+ }
488
+
489
+ def initialize(paylen_or_str = 0)
490
+ if(paylen_or_str.is_a?(Fixnum)) then
491
+ @ip_payload_len = paylen_or_str + 20 # tcp header
492
+ @ip_p = IPPROTO_TCP
493
+ @th_dport = 80
494
+ @th_sport = 0 # should be set by the daemon
495
+ @th_seq = 0 # should be set by the user, rand likely
496
+ @th_ack = 0 # should be set by the user, rand likely
497
+ @th_flags = TH_ACK # should be left alone, but could be set.
498
+ @th_win = 5180 # linux default.
499
+ @th_urp = 0 # not supported
500
+ @th_sum = 0 # should be set by the daemon
501
+ super(IPPROTO_TCP)
502
+ else
503
+ @th_sport, @th_dport, @th_seq, @th_ack, reserved, @th_flags, @th_win, @th_sum, @th_urp = paylen_or_str.unpack("nnNNCCnnn")
504
+ end
505
+ end
506
+ # @return [String] The packet in string form
507
+ def marshal
508
+ array_of_elements = [ @th_sport, @th_dport, @th_seq, @th_ack, 0x50, @th_flags, @th_win, @th_sum, @th_urp ]
509
+ raise "a TCP header field #{array_of_elements.index(nil)} was unset" if array_of_elements.include?(nil)
510
+ super + array_of_elements.pack("nnNNCCnnn")
511
+ # TODO plus payload length
512
+ end
513
+ end
514
+
515
+ class ICMP < IPv4
516
+ attr_reader :icmp_type, :icmp_code, :icmp_cksum
517
+ attr_reader :ip_p, :ip_payload_len
518
+
519
+ @@icmp_creators = Hash.new
520
+ @@creators[IPPROTO_ICMP] = lambda { |hdr|
521
+ icmp_type, icmp_code, icmp_cksum = hdr.unpack("CCn")
522
+ if(@@icmp_creators[icmp_type]) then
523
+ pkt = @@icmp_creators[icmp_type].call(hdr)
524
+ else
525
+ raise "unknown icmp type #%d" % icmp_type
526
+ end
527
+ }
528
+
529
+ def initialize(type_or_str)
530
+ if(type_or_str.is_a?(Fixnum)) then
531
+ @ip_p = IPPROTO_ICMP
532
+ @icmp_type = type_or_str
533
+ @icmp_code = 0
534
+ super(IPPROTO_ICMP)
535
+ else
536
+ @icmp_type, @icmp_code, @icmp_cksum = type_or_str.unpack("CCn")
537
+ end
538
+ end
539
+
540
+ # @return [String] The packet in string form
541
+ def marshal
542
+ @icmp_type or raise "type is nil"
543
+ @icmp_code or raise "code is nil"
544
+ @icmp_cksum = 0
545
+ super + [ @icmp_type, @icmp_code, @icmp_cksum ].pack("CCn")
546
+ end
547
+ # @return [String]
548
+ def to_s
549
+ super + ": ICMP: type %d code %d cksum %d" %[ @icmp_type, @icmp_code, @icmp_cksum ]
550
+ end
551
+ #instantiate echo or tstamp instead.
552
+ private :marshal
553
+ end
554
+
555
+ # Class for ping packets (echo and echo reply)
556
+ class ICMPecho < ICMP
557
+ # @return [Fixnum]
558
+ attr_reader :icmp_id, :icmp_seq
559
+ attr_writer :icmp_seq
560
+ @@icmp_creators[ICMP_ECHO] =
561
+ @@icmp_creators[ICMP_ECHOREPLY] = lambda { |hdr|
562
+ ICMPecho.new(hdr)
563
+ }
564
+ def initialize(paylen_or_str = 0)
565
+ if(paylen_or_str.is_a?(Fixnum)) then
566
+ if( paylen_or_str < 0) then raise "payload length must be >= 0" end
567
+ @ip_payload_len = paylen_or_str + 4 + 4
568
+ @icmp_id = 666
569
+ @icmp_seq = 1
570
+ super(ICMP_ECHO)
571
+ else
572
+ # x is skip forward a character.
573
+ @ip_payload_len = paylen_or_str.length - 8
574
+ @icmp_id, @icmp_seq = paylen_or_str.unpack("xxxxnn")
575
+ super(paylen_or_str)
576
+ end
577
+ end
578
+ # @return [String] The packet in string form
579
+ def marshal
580
+ super + [ @icmp_id, @icmp_seq ].pack("nn") + "\0" * ( @ip_payload_len - 4 - 4 )
581
+ end
582
+ # @return [String]
583
+ def to_s
584
+ super + ": ECHO: id %d seq %d len %d" % [ @icmp_id, @icmp_seq, @ip_payload_len ]
585
+ end
586
+ end
587
+
588
+ class ICMPtstamp < ICMP
589
+ # @return [Fixnum]
590
+ attr_reader :icmp_id, :icmp_seq
591
+ attr_reader :icmp_otime, :icmp_rtime, :icmp_ttime
592
+ attr_writer :icmp_seq
593
+ @@icmp_creators[ICMP_TIMESTAMP] =
594
+ @@icmp_creators[ICMP_TIMESTAMPREPLY] = lambda { |hdr|
595
+ ICMPtstamp.new(hdr)
596
+ }
597
+ def initialize(paylen_or_str = 0)
598
+ if(paylen_or_str.is_a?(Fixnum)) then
599
+ if( paylen_or_str < 0) then raise "payload length must be >= 0" end
600
+ @ip_payload_len = paylen_or_str + 4 + 16
601
+ @icmp_id = 666
602
+ @icmp_seq = 1
603
+ @icmp_otime = 0 # one of these three should probably be nonzero by the client, but I forget which.
604
+ @icmp_rtime = 0
605
+ @icmp_ttime = 0
606
+ super(ICMP_TIMESTAMP)
607
+ else
608
+ # x is skip forward a character.
609
+ @ip_payload_len = paylen_or_str.length - 20
610
+ @icmp_id, @icmp_seq, @icmp_otime, @icmp_rtime, @icmp_ttime = paylen_or_str.unpack("xxxxnnNNN")
611
+ super(paylen_or_str)
612
+ end
613
+ end
614
+ # @return [String] The packet in string form
615
+ def marshal
616
+ array_of_elements = [ @icmp_id, @icmp_seq, @icmp_otime, @icmp_rtime, @icmp_ttime ]
617
+ raise "an ICMP timestamp field #{array_of_elements.index(nil)} was unset" if array_of_elements.include?(nil)
618
+ super + array_of_elements.pack("nnNNN")
619
+ end
620
+ end
621
+
622
+ # mask request doesn't appear to be useful.
623
+ class ICMPmaskreq < ICMP
624
+ # @return [Fixnum]
625
+ attr_reader :icmp_id, :icmp_seq
626
+ attr_writer :icmp_seq
627
+ def initialize(payload_len = 0)
628
+ if( payload_len < 0) then raise "payload length must be 0" end
629
+ if( payload_len > 0) then raise "payload length must be 0" end
630
+ @ip_payload_len = payload_len + 12
631
+ @icmp_id = 678
632
+ @icmp_seq = 1
633
+ super(ICMP_MASKREQ)
634
+ end
635
+ # @return [String] The packet in string form
636
+ def marshal
637
+ super + [ @icmp_id, @icmp_seq, @icmp_otime, @icmp_rtime, @icmp_ttime ].pack("nnNNN")
638
+ end
639
+ end
640
+
641
+ # also handles time exceeded messages for now
642
+ # (same format, different code )
643
+ class ICMPunreach< ICMP
644
+ # @return [IPv4] The packet header embedded within the ICMP unreachable error message.
645
+ attr_reader :contents
646
+ @@icmp_creators[ICMP_UNREACH] = @@icmp_creators[ICMP_TIMXCEED] =
647
+ lambda { |hdr|
648
+ ICMPunreach.new(hdr)
649
+ }
650
+ # Can create an unreachable only from string contents, never from filling in fields given a size.
651
+ # param string [String] the contents of the received packet.
652
+ def initialize(string)
653
+ # first four are code, type, checksum.
654
+ # second four are undefined
655
+ @contents = IPv4.creator(string[8..-1])
656
+ super(string)
657
+ end
658
+ # Cannot marshal an unreachable packet for transmission; raises an exception.
659
+ # @return [void]
660
+ def marshal
661
+ raise "not supported"
662
+ end
663
+ # @return [String] formats the packet and the embedded packet as a string.
664
+ def to_s
665
+ super + " ( " + @contents.to_s + " )"
666
+ end
667
+ end
668
+
669
+ class NTP < UDP
670
+ # @return [Fixnum]
671
+ attr_accessor :leap_indicator, :version_number, :mode, :stratum
672
+ attr_accessor :poll_interval, :precision, :root_delay
673
+ attr_accessor :root_dispersion, :reference_identifier
674
+ attr_accessor :reference_timestamp, :originate_timestamp
675
+ attr_accessor :receive_timestamp, :transmit_timestamp
676
+
677
+ def initialize(paylen_or_str = 0)
678
+ if(paylen_or_str.is_a?(Fixnum)) then
679
+ if( paylen_or_str < 0) then raise "payload length must be >= 0" end
680
+
681
+ @leap_indicator = 3 # alarm condition / clock not synchronized
682
+ @version_number = 4 # unclear if it should be.
683
+ @mode = 3 # client
684
+ @stratum = 0 # unspecified
685
+ @poll_interval = 4 # 16s
686
+ @precision = -6 # emulate ntpdate, though -20 is more likely
687
+ @root_delay = 1.0 # packed funny.
688
+ @root_dispersion = 1.0 # packed funny.
689
+ @reference_identifier = '0.0.0.0'
690
+ @reference_timestamp = 0.0
691
+ @originate_timestamp = 0.0
692
+ @receive_timestamp = 0.0
693
+ @transmit_timestamp = 0.0
694
+
695
+ super( paylen_or_str + 48 )
696
+
697
+ @uh_dport = 123
698
+
699
+ else
700
+ ntp_unmarshal(paylen_or_str)
701
+ end
702
+ end
703
+
704
+ def ntp_unmarshal(str)
705
+
706
+ ntp_lvm, @stratum, @poll_interval, @precision,
707
+ root_delay1,root_delay2,
708
+ root_dispersion1,root_dispersion2,
709
+ r1,r2,r3,r4,
710
+ reference_timestamp1,reference_timestamp2,
711
+ originate_timestamp1,originate_timestamp2,
712
+ receive_timestamp1,receive_timestamp2,
713
+ transmit_timestamp1, transmit_timestamp2 = str.unpack( "cccc" +
714
+ "nn" +
715
+ "nn" +
716
+ "cccc" +
717
+ "NN" +
718
+ "NN" +
719
+ "NN" +
720
+ "NN");
721
+
722
+ @leap_indicator = (ntp_lvm >> 6) & 0xfc
723
+ @version_number = (ntp_lvm & 0x38) >> 3
724
+ @mode = ntp_lvm & 0x07
725
+ @root_delay = root_delay1+root_delay2/65536.0
726
+ @root_dispersion = root_dispersion1+root_dispersion2/65536.0
727
+ @reference_identifier = "%d.%d.%d.%d"% [r1,r2,r3,r4].map{ |i| (i<0)?i+256:i }
728
+ @reference_timestamp = reference_timestamp1 + 0.0 + reference_timestamp2/4294967296.0
729
+ @originate_timestamp = originate_timestamp1 + originate_timestamp2/4294967296.0
730
+ @receive_timestamp = receive_timestamp1 + receive_timestamp2/4294967296.0
731
+ @transmit_timestamp = transmit_timestamp1 + transmit_timestamp2/4294967296.0
732
+
733
+ end
734
+
735
+ def float_to_two_shorts(flt)
736
+ flt == nil and raise "need a float"
737
+ [ flt.to_i, ((flt - flt.to_i) * 65536).to_i ]
738
+ end
739
+
740
+ def float_to_two_longs(flt)
741
+ flt == nil and raise "need a float"
742
+ [ flt.to_i, ((flt - flt.to_i) * 4294967296).to_i ]
743
+ end
744
+
745
+ def to_bits(int, bits)
746
+ ret = ""
747
+ (1..bits).each do |b|
748
+ ret += (int % 2).to_s
749
+ int = int / 2
750
+ end
751
+ ret
752
+ end
753
+
754
+
755
+ # @return [String] The packet in string form
756
+ def marshal
757
+ if ($VERBOSE) then
758
+ puts "marshaling with IP payload length %d" % ip_payload_len
759
+ end
760
+ super + [ @leap_indicator * 64 + @version_number * 8 + @mode, @stratum, @poll_interval, @precision,
761
+ float_to_two_shorts(@root_delay),
762
+ float_to_two_shorts(@root_dispersion),
763
+ @reference_identifier.to_i,
764
+ float_to_two_longs(@reference_timestamp),
765
+ float_to_two_longs(@originate_timestamp),
766
+ float_to_two_longs(@receive_timestamp),
767
+ float_to_two_longs(@transmit_timestamp) ].flatten.pack("cccc" +
768
+ "nn" +
769
+ "nn" +
770
+ "N" +
771
+ "NN" +
772
+ "NN" +
773
+ "NN" +
774
+ "NN") + "\0" * ( @uh_ulen - 8 - 48 )
775
+
776
+ end
777
+ end
778
+
779
+ # somewhat more friendly to interpret the type of the message first.
780
+ class ICMPparamter_problem < ICMP
781
+ # @return [Fixnum]
782
+ attr_reader :icmp_id, :icmp_seq
783
+ attr_writer :icmp_seq
784
+ @@icmp_creators[ICMP_PARAMETERPROB] = lambda { |hdr|
785
+ raise "ICMP_PARAMETERPROB unsupported."
786
+ }
787
+ end
788
+
789
+ class ProbeResponse
790
+ # this method implemented in pure ruby regardless of interpreter state.
791
+ # @return [String]
792
+ def to_s
793
+ "%s @%5.6f -> %s +%5.6f" % [@probe, @probe.time, (@response or "<none>"), (rtt or "-1")]
794
+ end
795
+ end
796
+ end
797
+
798
+ # Local Variables:
799
+ # compile-command: "rake test"
800
+ # End: