tlspretense 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. data/.document +6 -0
  2. data/.gitignore +7 -0
  3. data/.rspec +1 -0
  4. data/Gemfile +2 -0
  5. data/Gemfile.lock +41 -0
  6. data/LICENSE.txt +20 -0
  7. data/README.rdoc +231 -0
  8. data/Rakefile +44 -0
  9. data/bin/makeder.sh +6 -0
  10. data/bin/tlspretense +7 -0
  11. data/bin/view.sh +3 -0
  12. data/doc/general_setup.rdoc +288 -0
  13. data/doc/linux_setup.rdoc +64 -0
  14. data/lib/certmaker.rb +61 -0
  15. data/lib/certmaker/certificate_factory.rb +106 -0
  16. data/lib/certmaker/certificate_suite_generator.rb +120 -0
  17. data/lib/certmaker/ext_core/hash_indifferent_fetch.rb +12 -0
  18. data/lib/certmaker/runner.rb +27 -0
  19. data/lib/certmaker/tasks.rb +20 -0
  20. data/lib/packetthief.rb +167 -0
  21. data/lib/packetthief/handlers.rb +14 -0
  22. data/lib/packetthief/handlers/abstract_ssl_handler.rb +249 -0
  23. data/lib/packetthief/handlers/proxy_redirector.rb +26 -0
  24. data/lib/packetthief/handlers/ssl_client.rb +87 -0
  25. data/lib/packetthief/handlers/ssl_server.rb +174 -0
  26. data/lib/packetthief/handlers/ssl_smart_proxy.rb +143 -0
  27. data/lib/packetthief/handlers/ssl_transparent_proxy.rb +225 -0
  28. data/lib/packetthief/handlers/transparent_proxy.rb +183 -0
  29. data/lib/packetthief/impl.rb +11 -0
  30. data/lib/packetthief/impl/ipfw.rb +140 -0
  31. data/lib/packetthief/impl/manual.rb +54 -0
  32. data/lib/packetthief/impl/netfilter.rb +109 -0
  33. data/lib/packetthief/impl/pf_divert.rb +168 -0
  34. data/lib/packetthief/impl/pf_rdr.rb +192 -0
  35. data/lib/packetthief/logging.rb +49 -0
  36. data/lib/packetthief/redirect_rule.rb +29 -0
  37. data/lib/packetthief/util.rb +36 -0
  38. data/lib/ssl_test.rb +21 -0
  39. data/lib/ssl_test/app_context.rb +17 -0
  40. data/lib/ssl_test/certificate_manager.rb +33 -0
  41. data/lib/ssl_test/config.rb +79 -0
  42. data/lib/ssl_test/ext_core/io_raw_input.rb +31 -0
  43. data/lib/ssl_test/input_handler.rb +35 -0
  44. data/lib/ssl_test/runner.rb +110 -0
  45. data/lib/ssl_test/runner_options.rb +68 -0
  46. data/lib/ssl_test/ssl_test_case.rb +46 -0
  47. data/lib/ssl_test/ssl_test_report.rb +24 -0
  48. data/lib/ssl_test/ssl_test_result.rb +30 -0
  49. data/lib/ssl_test/test_listener.rb +140 -0
  50. data/lib/ssl_test/test_manager.rb +116 -0
  51. data/lib/tlspretense.rb +13 -0
  52. data/lib/tlspretense/app.rb +52 -0
  53. data/lib/tlspretense/init_runner.rb +115 -0
  54. data/lib/tlspretense/skel/ca/goodcacert.pem +19 -0
  55. data/lib/tlspretense/skel/ca/goodcakey.pem +27 -0
  56. data/lib/tlspretense/skel/config.yml +523 -0
  57. data/lib/tlspretense/version.rb +3 -0
  58. data/packetthief_examples/em_ssl_test.rb +73 -0
  59. data/packetthief_examples/redirector.rb +29 -0
  60. data/packetthief_examples/setup_iptables.sh +24 -0
  61. data/packetthief_examples/ssl_client_simple.rb +27 -0
  62. data/packetthief_examples/ssl_server_simple.rb +44 -0
  63. data/packetthief_examples/ssl_smart_proxy.rb +115 -0
  64. data/packetthief_examples/ssl_transparent_proxy.rb +97 -0
  65. data/packetthief_examples/transparent_proxy.rb +56 -0
  66. data/spec/packetthief/impl/ipfw_spec.rb +98 -0
  67. data/spec/packetthief/impl/manual_spec.rb +65 -0
  68. data/spec/packetthief/impl/netfilter_spec.rb +66 -0
  69. data/spec/packetthief/impl/pf_divert_spec.rb +82 -0
  70. data/spec/packetthief/impl/pf_rdr_spec.rb +133 -0
  71. data/spec/packetthief/logging_spec.rb +78 -0
  72. data/spec/packetthief_spec.rb +47 -0
  73. data/spec/spec_helper.rb +53 -0
  74. data/spec/ssl_test/certificate_manager_spec.rb +222 -0
  75. data/spec/ssl_test/config_spec.rb +76 -0
  76. data/spec/ssl_test/runner_spec.rb +360 -0
  77. data/spec/ssl_test/ssl_test_case_spec.rb +113 -0
  78. data/spec/ssl_test/test_listener_spec.rb +199 -0
  79. data/spec/ssl_test/test_manager_spec.rb +324 -0
  80. data/tlspretense.gemspec +35 -0
  81. metadata +262 -0
@@ -0,0 +1,143 @@
1
+ module PacketThief
2
+ module Handlers
3
+
4
+ # This SSL proxy needs a CA certificate (or chain) and private key. It then
5
+ # uses that information to automatically generate host certificates. It
6
+ # creates a host certificate by performing a pre-flight request to the
7
+ # original destination to get its certificate. It then doctors that
8
+ # certificate so that it's keypair is the supplied keypair, and so that it
9
+ # appears to be issued by the CA or last CA in the chain. Note that the
10
+ # supplied key must correspond to the last signing certificate, and that
11
+ # the key will be used as the doctored certificate's new key.
12
+ class SSLSmartProxy < SSLTransparentProxy
13
+
14
+ # How long to wait before giving up on a preflight request. Defaults to 5
15
+ # seconds.
16
+ attr_accessor :preflight_timeout
17
+
18
+ def initialize(tcpsocket, ca_chain, key, logger=nil)
19
+ super(tcpsocket, logger)
20
+
21
+ @preflight_timeout = 5
22
+ if ca_chain.kind_of? Array
23
+ @ca_chain = ca_chain
24
+ else
25
+ @ca_chain = [ca_chain]
26
+ end
27
+ @key = key
28
+
29
+ begin
30
+ # preflight the original destination.
31
+ @ctx.cert = lookup_cert
32
+ @ctx.extra_chain_cert = @ca_chain
33
+ @ctx.key = @key
34
+ rescue OpenSSL::SSL::SSLError, Errno::ECONNREFUSED, Errno::ETIMEDOUT => e
35
+ logerror "initialize: Failed to look up cert", :error => e
36
+ close_connection
37
+ end
38
+ end
39
+
40
+ # def client_connected
41
+ # # don't try to connect to dest here.
42
+ # end
43
+ #
44
+
45
+ def servername_cb(sslsock, hostname)
46
+ super
47
+ newctx = sslsock.context.dup
48
+ newctx.cert = lookup_cert(hostname)
49
+ newctx.extra_chain_cert = @ca_chain
50
+ newctx.key = @key
51
+ newctx
52
+ end
53
+
54
+ # Requests a certificate from the original destination.
55
+ def preflight_for_cert(hostname=nil)
56
+ logdebug "prefilight for: #{hostname}"
57
+ begin
58
+ pfctx = OpenSSL::SSL::SSLContext.new
59
+ pfctx.verify_mode = OpenSSL::SSL::VERIFY_NONE
60
+ # Try to use a non-blocking Socket to create a short timeout.
61
+ pfs = Socket.new(:AF_INET, :SOCK_STREAM)
62
+ begin
63
+ pfs.connect_nonblock(Socket.sockaddr_in(dest_port, dest_host))
64
+ rescue Errno::EINPROGRESS
65
+ IO.select(nil, [pfs], nil, preflight_timeout) or raise Errno::ETIMEDOUT, "Connection to #{dest_host}:#{dest_port} timed out after #{preflight_timeout} seconds"
66
+ end
67
+ logdebug "preflight tcp socket connected"
68
+ pfssl = OpenSSL::SSL::SSLSocket.new(pfs, pfctx)
69
+ pfssl.hostname = hostname if hostname and pfssl.respond_to? :hostname
70
+ begin
71
+ pfssl.connect_nonblock
72
+ rescue IO::WaitReadable, IO::WaitWritable
73
+ IO.select([pfssl],[pfssl],nil,preflight_timeout) or raise Errno::ETIMEDOUT, "SSL handshake to #{dest_host}:#{dest_port} timed out #{preflight_timeout} seconds"
74
+ retry
75
+ end
76
+ logdebug "preflight complete"
77
+ return pfssl.peer_cert
78
+ rescue OpenSSL::SSL::SSLError, Errno::ECONNREFUSED, Errno::ETIMEDOUT => e
79
+ logerror "Error during preflight SSL connection: #{e} (#{e.class})"
80
+ raise
81
+ ensure
82
+ pfssl.close if pfssl
83
+ end
84
+ end
85
+
86
+
87
+ # Replace the issuer, the public key, and the signature.
88
+ def doctor_cert(oldcert, cacert, key)
89
+ newcert = oldcert.dup
90
+ newcert.issuer = cacert.subject
91
+ newcert.public_key = key.public_key
92
+
93
+ exts = newcert.extensions.dup
94
+ exts.each_index do |i|
95
+ if exts[i].oid == "authorityKeyIdentifier"
96
+ ef = OpenSSL::X509::ExtensionFactory.new
97
+ ef.subject_certificate = newcert
98
+ ef.issuer_certificate = cacert
99
+ newe = ef.create_ext_from_string("authorityKeyIdentifier=keyid:always")
100
+ exts[i] = newe
101
+ end
102
+ end
103
+ newcert.extensions = exts
104
+
105
+ sigalg = case newcert.signature_algorithm
106
+ when /MD5/i
107
+ OpenSSL::Digest::MD5.new
108
+ when /SHA1/i
109
+ OpenSSL::Digest::SHA1.new
110
+ when /SHA256/i
111
+ OpenSSL::Digest::SHA256.new
112
+ else
113
+ raise "Unsupported signing algorithm: #{@ctx.cert.signing_algorithm}"
114
+ end
115
+ newcert.sign(key, sigalg)
116
+ return newcert
117
+ end
118
+
119
+ # Check a class-level cache for an existing doctored certificate that
120
+ # corresponds to the requested hostname (IP address for non-SNI or
121
+ # pre-SNI lookup, and hostnames from SNI lookup).
122
+ def lookup_cert(hostname=nil)
123
+ @@certcache ||= {}
124
+
125
+ if hostname
126
+ cachekey = "#{hostname}:#{dest_port}"
127
+ else
128
+ cachekey = "#{dest_host}:#{dest_port}"
129
+ end
130
+
131
+ unless @@certcache.has_key? cachekey
132
+ logdebug "lookup_cert: cache miss, looking up and doctoring actual cert", :dest => cachekey
133
+ @@certcache[cachekey] = doctor_cert(preflight_for_cert(hostname), @ca_chain[0], @key)
134
+ else
135
+ logdebug "lookup_cert: cache hit", :dest => cachekey
136
+ end
137
+ logdebug "lookup_cert: returning", :subject => @@certcache[cachekey].subject
138
+ @@certcache[cachekey]
139
+ end
140
+
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,225 @@
1
+ module PacketThief
2
+ module Handlers
3
+
4
+ # Provides a transparent proxy for any TCP connection.
5
+ class SSLTransparentProxy < SSLServer
6
+
7
+ # Represents a connection out to the original destination.
8
+ class SSLProxyConnection < SSLClient
9
+ # Boolean that represents whether this handler has started to
10
+ # close/unbind. Used to ensure there is no unbind-loop between the two
11
+ # connections that make up the proxy.
12
+ attr_accessor :closed
13
+
14
+ # Boolean that represents whether the connection has connected yet.
15
+ attr_accessor :connected
16
+
17
+ # Sets up references to the client proxy connection handler that created
18
+ # this handler.
19
+ def initialize(tcpsocket, client_conn, ctx)
20
+ super(tcpsocket)
21
+ @client = client_conn
22
+ @ctx = ctx
23
+
24
+ @connected = false
25
+ @closed = false
26
+ sni_hostname = @client.dest_hostname if @client.dest_hostname
27
+ end
28
+
29
+ # send on successful handshake instead of on post_init.
30
+ def tls_successful_handshake
31
+ @client.dest_connected
32
+ @client._send_buffer
33
+ end
34
+
35
+ def tls_failed_handshake(e)
36
+ @client.dest_handshake_failed(e)
37
+ end
38
+
39
+ # Transmit data sent by the destinaton to the client.
40
+ def receive_data(data)
41
+ @client.dest_recv(data)
42
+ end
43
+
44
+ # Start the closing process and close the other connection if it is not
45
+ # already closing.
46
+ def unbind
47
+ @client.dest_closed
48
+ self.closed = true
49
+ @client.dest = nil
50
+ @client.close_connection_after_writing if @client and not @client.closed
51
+ end
52
+
53
+ end
54
+
55
+ # This holds a reference to the connection to the destination. If it is
56
+ # null, it hasn't been created yet.
57
+ attr_accessor :dest
58
+
59
+ # This holds a reference to the connection to the client. It is actually
60
+ # self.
61
+ attr_accessor :client
62
+
63
+ attr_reader :client_host, :client_port
64
+
65
+ # Override these before connecting to dest to change the dest connection.
66
+ attr_accessor :dest_host, :dest_port
67
+
68
+ # An internal buffer of packet data received from the client. It will grow until
69
+ # the destination connection connects.
70
+ attr_accessor :buffer
71
+
72
+ # Boolean that represents whether this handler has started to
73
+ # close/unbind. Used to ensure there is no unbind-loop between the two
74
+ # connections that make up the proxy.
75
+ attr_accessor :closed
76
+
77
+ # If a client specifies a TLS hostname extension (SNI) as the hostname,
78
+ # then we can forward that fact on to the real server. We can also use it
79
+ # to choose a certificate to present.
80
+ attr_accessor :dest_hostname
81
+
82
+ # The SSLContext that will be used on the connection to the destination.
83
+ # Initially, its verify_mode is set to OpenSSL::SSL::VERIFY_NONE.
84
+ attr_accessor :dest_ctx
85
+
86
+ def initialize(tcpsocket, logger=nil)
87
+ super
88
+ @closed = false
89
+
90
+ @client = self
91
+ @dest = nil
92
+
93
+ @buffer = []
94
+ @@activeconns ||= {}
95
+
96
+ @client_port, @client_host = Socket.unpack_sockaddr_in(get_peername)
97
+ @dest_port, @dest_host = PacketThief.original_dest(self)
98
+
99
+ if @@activeconns.has_key? "#{client_host}:#{client_port}"
100
+ logwarn "loop detected! Stopping the loop."
101
+ close_connection
102
+ return
103
+ end
104
+
105
+ @dest_ctx = OpenSSL::SSL::SSLContext.new
106
+ @dest_ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE
107
+
108
+ end
109
+
110
+ # Just calls client_connected to keep things straightforward.
111
+ def tls_successful_handshake
112
+ client_connected
113
+ end
114
+
115
+ def receive_data(data)
116
+ client_recv data
117
+ end
118
+
119
+ # Start the closing process and close the other connection if it is not
120
+ # already closing.
121
+ def unbind
122
+ client_closed
123
+ @@activeconns.delete "#{client_host}:#{client_port}"
124
+ self.closed = true
125
+ # @dest.client = nil if @dest
126
+ @dest.close_connection_after_writing if @dest and not @dest.closed
127
+ end
128
+
129
+ # Initiate the connection to @dest_host:@dest_port.
130
+ def connect_to_dest
131
+ return if @dest
132
+ @dest = SSLProxyConnection.connect(@dest_host, @dest_port, self, @dest_ctx)
133
+ newport, newhost = Socket::unpack_sockaddr_in(@dest.get_sockname)
134
+ # Add the new connection to the list to prevent loops.
135
+ @@activeconns["#{newhost}:#{newport}"] = "#{dest_host}:#{dest_port}"
136
+ end
137
+
138
+ def _send_buffer
139
+ @buffer.each do |pkt|
140
+ @dest.send_data pkt
141
+ end
142
+ @buffer = []
143
+ end
144
+
145
+
146
+ # Queues up data to send to the remote host, only sending it if the
147
+ # connection to the remote host exists.
148
+ def send_to_dest(data)
149
+ @buffer << data
150
+ _send_buffer if @dest
151
+ end
152
+
153
+ # Sends data back to the client
154
+ def send_to_client(data)
155
+ send_data data
156
+ end
157
+
158
+ # Returns the certificate chain for the destination, or nil if the
159
+ # destination connection does not exist yet.
160
+ def dest_cert_chain
161
+ return @dest.sslsocket.peer_cert_chain if @dest
162
+ nil
163
+ end
164
+
165
+ #### Callbacks
166
+
167
+ # Set _dest_hostname_ in addition to the default behavior.
168
+ def servername_cb(sslsock, hostname)
169
+ @dest_hostname = hostname
170
+
171
+ super(sslsock, hostname)
172
+ end
173
+
174
+ # This method is called when a client connects, and the TLS handhsake has
175
+ # completed. The default behavior is to begin initating the connection to
176
+ # the original destination. Override this method to change its behavior.
177
+ def client_connected
178
+ connect_to_dest
179
+ end
180
+
181
+ # This method is called when the TLS handshake between the client and the
182
+ # proxy fails. It does nothing by default.
183
+ def client_handshake_failed
184
+ end
185
+
186
+ # This method is called when the proxy receives data from the client
187
+ # connection. The default behavior is to call send_to_dest(data) in order
188
+ # to foward the data on to the original destination. Override this method
189
+ # to analyze the data, or modify it before sending it on.
190
+ def client_recv(data)
191
+ send_to_dest data
192
+ end
193
+
194
+ # Called when the client connection closes. At present, it only provides
195
+ # informational utility.
196
+ def client_closed
197
+ end
198
+
199
+ # Called when the connection to and the TLS handshake between the proxy
200
+ # and the destination succeeds. The default behavior does nothing.
201
+ def dest_connected
202
+ end
203
+
204
+ # Called when the TLS handshake between the proxy and the destination
205
+ # fails.
206
+ def dest_handshake_failed(e)
207
+ end
208
+
209
+ # Called when the proxy receives data from the destination connection.
210
+ # The default behavior calls #dest_recv() to send the data to the client.
211
+ #
212
+ # Override it to analyze or modify the data.
213
+ def dest_recv(data)
214
+ send_to_client data
215
+ end
216
+
217
+ # Called when the original destination connection closes. At present, it only provides
218
+ # informational utility.
219
+ def dest_closed
220
+ end
221
+
222
+ end
223
+
224
+ end
225
+ end
@@ -0,0 +1,183 @@
1
+ module PacketThief
2
+ module Handlers
3
+
4
+ # Provides a transparent proxy for any TCP connection.
5
+ class TransparentProxy < ::EM::Connection
6
+ include Logging
7
+
8
+ # Represents a connection out to the original destination.
9
+ module ProxyConnection
10
+ # Boolean that represents whether this handler has started to
11
+ # close/unbind. Used to ensure there is no unbind-loop between the two
12
+ # connections that make up the proxy.
13
+ attr_accessor :closing
14
+
15
+ # Boolean that represents whether the connection has connected yet.
16
+ attr_accessor :connected
17
+
18
+ # Sets up references to the client proxy connection handler that created
19
+ # this handler.
20
+ def initialize(client_conn)
21
+ @client = client_conn
22
+
23
+ @connected = false
24
+ @closing = false
25
+ end
26
+
27
+ def post_init
28
+ @client._send_buffer
29
+ end
30
+
31
+ # Transmit data sent by the destinaton to the client.
32
+ def receive_data(data)
33
+ @client.dest_recv(data)
34
+ end
35
+
36
+ # Start the closing process and close the other connection if it is not
37
+ # already closing.
38
+ def unbind
39
+ @client.dest_closed
40
+ self.closing = true
41
+ @client.close_connection_after_writing if @client and not @client.closing
42
+ end
43
+
44
+ end
45
+
46
+ # This holds a reference to the connection to the destination. If it is
47
+ # null, it hasn't been created yet.
48
+ attr_accessor :dest
49
+
50
+ # This holds a reference to the connection to the client. It is actually
51
+ # self.
52
+ attr_accessor :client
53
+
54
+ attr_reader :client_host, :client_port
55
+
56
+ # Override these before connecting to dest to change the dest connection.
57
+ attr_accessor :dest_host, :dest_port
58
+
59
+ # An internal buffer of packet data received from the client. It will grow until
60
+ # the destination connection connects.
61
+ attr_accessor :buffer
62
+
63
+ # Boolean that represents whether this handler has started to
64
+ # close/unbind. Used to ensure there is no unbind-loop between the two
65
+ # connections that make up the proxy.
66
+ attr_accessor :closing
67
+
68
+ # When the proxy should connect to a destination.
69
+ attr_accessor :when_to_connect_to_dest
70
+
71
+ def initialize(log=nil)
72
+ @logger = log
73
+ end
74
+
75
+ def post_init
76
+ @closing = false
77
+
78
+ @client = self
79
+ @dest = nil
80
+
81
+ @buffer = []
82
+ @@activeconns ||= {}
83
+
84
+ @client_port, @client_host = Socket.unpack_sockaddr_in(get_peername)
85
+ @dest_port, @dest_host = PacketThief.original_dest(self)
86
+
87
+ logdebug "Client connected", :client_host => client_host, :client => "#{@client_host}:#{@client_port}", :orig_dest => "#{@dest_host}:#{@dest_port}"
88
+
89
+ if @@activeconns.has_key? "#{client_host}:#{client_port}"
90
+ puts "Warning: loop detected! Stopping the loop."
91
+ close_connection
92
+ return
93
+ end
94
+
95
+ client_connected
96
+ end
97
+
98
+ def receive_data(data)
99
+ client_recv data
100
+ end
101
+
102
+ # Start the closing process and close the other connection if it is not
103
+ # already closing.
104
+ def unbind
105
+ client_closed
106
+ @@activeconns.delete "#{client_host}:#{client_port}"
107
+ self.closing = true
108
+ @dest.close_connection_after_writing if @dest and not @dest.closing
109
+ end
110
+
111
+
112
+ # Initiate the connection to @dest_host:@dest_port.
113
+ def connect_to_dest
114
+ logdebug "Connecting to #{@dest_host}:#{@dest_port}"
115
+ @dest = ::EM.connect(@dest_host, @dest_port, ProxyConnection, self)
116
+ newport, newhost = Socket::unpack_sockaddr_in(@dest.get_sockname)
117
+ # Add the new connection to the list to prevent loops.
118
+ @@activeconns["#{newhost}:#{newport}"] = "#{dest_host}:#{dest_port}"
119
+ end
120
+
121
+ def _send_buffer
122
+ @buffer.each do |pkt|
123
+ @dest.send_data pkt
124
+ end
125
+ @buffer = []
126
+ end
127
+
128
+
129
+ # Queues up data to send to the remote host, only sending it if the
130
+ # connection to the remote host exists.
131
+ def send_to_dest(data)
132
+ logdebug "sending to dest", :data => data
133
+ @buffer << data
134
+ _send_buffer if @dest
135
+ end
136
+
137
+ # Sends data back to the client
138
+ def send_to_client(data)
139
+ send_data data
140
+ end
141
+
142
+
143
+ # This method is called when a client connects. The default behavior is
144
+ # to begin initating the connection to the original destination. Override
145
+ # this method to change its behavior.
146
+ def client_connected
147
+ connect_to_dest
148
+ end
149
+
150
+ # This method is called when the proxy receives data from the client
151
+ # connection. The default behavior is to call send_to_dest(data) in order
152
+ # to foward the data on to the original destination. Override this method
153
+ # to analyze the data, or modify it before sending it on.
154
+ def client_recv(data)
155
+ logdebug("received from client", :data => data)
156
+ send_to_dest data
157
+ end
158
+
159
+ # Called when the proxy receives data from the destination connection.
160
+ # The default behavior calls #dest_recv() to send the data to the client.
161
+ #
162
+ # Override it to analyze or modify the data.
163
+ def dest_recv(data)
164
+ logdebug("received from dest", :data => data)
165
+ send_to_client data
166
+ end
167
+
168
+ # Called when the client connection closes. At present, it only provides
169
+ # informational utility.
170
+ def client_closed
171
+ logdebug("client closed connection")
172
+ end
173
+
174
+ # Called when the original destination connection closes. At present, it only provides
175
+ # informational utility.
176
+ def dest_closed
177
+ logdebug("dest closed connection")
178
+ end
179
+
180
+ end
181
+
182
+ end
183
+ end