Permalink
Browse files

Major rewrite, now works with TOR and does not leak DNS queries

  • Loading branch information...
1 parent d14ac35 commit 2145ac9ca4a2a9cdc04e914687eca6bd1ccb19e7 @meh meh committed with Sep 28, 2011
Showing with 143 additions and 80 deletions.
  1. +1 −1 README.md
  2. +32 −0 lib/em-socksify/errors.rb
  3. +109 −78 lib/em-socksify/socksify.rb
  4. +1 −1 spec/socksify_spec.rb
View
@@ -47,4 +47,4 @@ socksify(destination_host, destination_port, username, password)
# License
(The MIT License)
-Copyright © 2011 Ilya Grigorik
+Copyright © 2011 Ilya Grigorik
View
@@ -0,0 +1,32 @@
+class SOCKSError < Exception
+ def self.define (message)
+ Class.new(self) {
+ def initialize
+ super(message)
+ end
+ }
+ end
+
+ ServerFailure = SOCKSError.define('general SOCKS server failure')
+ NotAllowed = SOCKSError.define('connection not allowed by ruleset')
+ NetworkUnreachable = SOCKSError.define('Network unreachable')
+ HostUnreachable = SOCKSError.define('Host unreachable')
+ ConnectionRefused = SOCKSError.define('Connection refused')
+ TTLExpired = SOCKSError.define('TTL expired')
+ CommandNotSupported = SOCKSError.define('Command not supported')
+ AddressTypeNotSupported = SOCKSError.define('Address type not supported')
+
+ def self.for_response_code(code)
+ case code.is_a?(String) ? code.ord : code
+ when 1 then ServerFailure
+ when 2 then NotAllowed
+ when 3 then NetworkUnreachable
+ when 4 then HostUnreachable
+ when 5 then ConnectionRefused
+ when 6 then TTLExpired
+ when 7 then CommandNotSupported
+ when 8 then AddressTypeNotSupported
+ else self
+ end
+ end
+end
View
@@ -1,125 +1,156 @@
+require 'em-socksify/errors'
+
module EventMachine
module Socksify
+ def socksify(host, port, username = nil, password = nil, version = 5, &blk)
+ @socks_target_host = host
+ @socks_target_port = port
+ @socks_username = username
+ @socks_password = password
+ @socks_version = version
+ @socks_callback = blk
+ @socks_data = ''
+
+ socks_hook
+ socks_send_handshake
+ end
- def socksify(host, port, username = nil, password = nil, &blk)
- @host = host
- @port = port
- @username = username
- @password = password
- @callback = blk
+ def socks_hook
+ if @socks_version == 5
+ extend SOCKS5
+ else
+ raise ArgumentError, 'SOCKS version unsupported'
+ end
class << self
- def receive_data(data); proxy_receive_data(data); end
+ alias receive_data socks_receive_data
end
-
- send_socks_handshake
end
- def proxy_receive_data(data)
- @data ||= ''
- @data << data
- parse_socks_response
+ def socks_unhook(ip = nil)
+ class << self
+ remove_method :receive_data
+ end
+
+ callback = @socks_callback
+
+ instance_variables.each {|name|
+ remove_instance_variable name if name.to_s.start_with?('@socks_')
+ }
+
+ callback.call(ip)
end
- def send_socks_handshake
- # Method Negotiation as described on
- # http://www.faqs.org/rfcs/rfc1928.html Section 3
- @socks_state = :method_negotiation
+ def socks_receive_data(data)
+ @socks_data << data
- methods = socks_methods
- send_data [5, methods.size].pack('CC') + methods.pack('C*')
+ socks_parse_response
end
- def send_socks_connect_request
- send_data [5, 1, 0].pack('CCC')
+ module SOCKS5
+ def socks_send_handshake
+ # Method Negotiation as described on
+ # http://www.faqs.org/rfcs/rfc1928.html Section 3
+ @socks_state = :method_negotiation
- begin
- # TODO: Implement address types for IPv6 and Domain
- # TODO: resolve domain through the proxy
- send_data [1, Socket.gethostbyname(@host).last].pack('CA4')
- rescue
- send_data [3, @host.size, @host].pack('CCA*')
+ socks_methods.tap {|methods|
+ send_data [5, methods.size].pack('CC') + methods.pack('C*')
+ }
end
- send_data [@port].pack('n')
- end
+ def socks_send_connect_request
+ @socks_state = :connecting
+
+ send_data [5, 1, 0].pack('CCC')
+
+ if matches = @socks_target_host.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/)
+ send_data "\xF1\x00\x01" + matches.to_a[1 .. -1].map { |s| s.to_i }.pack('CCCC')
+ elsif @socks_target_host =~ /^[:0-9a-f]+$/
+ raise SOCKSError, 'TCP/IPv6 over SOCKS is not yet supported (inet_pton missing in Ruby & not supported by Tor'
+ else
+ send_data [3, @socks_target_host.size, @socks_target_host].pack('CCA*')
+ end
+
+ send_data [@socks_target_port].pack('n')
+ end
+
+ def socks_send_authentication
+ @socks_state = :authenticating
+
+ send_data [5,
+ @socks_username.length, @socks_username,
+ @socks_password.length, @socks_password
+ ].pack('CCA*CA*')
+ end
- private
+ private
# parses socks 5 server responses as specified
# on http://www.faqs.org/rfcs/rfc1928.html
- def parse_socks_response
- if @socks_state == :method_negotiation
- return if not @data.size >= 2
+ def socks_parse_response
+ case @socks_state
+ when :method_negotiation
+ return unless @socks_data.size >= 2
- _, method = @data.slice!(0,2).unpack('CC')
+ _, method = @socks_data.slice!(0, 2).unpack('CC')
if socks_methods.include?(method)
- if method == 0
- @socks_state = :connecting
- send_socks_connect_request
-
- elsif method == 2
- @socks_state = :authenticating
- send_data [5, @username.length, @username, @password.length, @password].pack('CCA*CA*')
+ case method
+ when 0 then socks_send_connect_request
+ when 2 then socks_send_authentication
end
-
else
- fail("proxy did not accept method")
+ raise SOCKSError, 'proxy did not accept method'
end
- elsif @socks_state == :authenticating
- return if not @data.size >= 2
+ when :authenticating
+ return unless @socks_data.size >= 2
- _, status_code = @data.slice!(0, 2).unpack('CC')
+ socks_version, status_code = @socks_data.slice!(0, 2).unpack('CC')
- if status_code == 0 # success
- @socks_state = :connecting
- send_socks_connect_request
+ raise SOCKSError, "SOCKS version 5 not supported" unless socks_version == 5
+ raise SOCKSError, 'access denied by proxy' unless status_code == 0
- else # error
- fail "access denied by proxy"
- end
+ send_socks_connect_request
- elsif @socks_state == :connecting
- return if not @data.size >= 10
+ when :connecting
+ return unless @socks_data.size >= 2
- _, response_code, _, address_type, _, _ = @data.slice(0, 10).unpack('CCCCNn')
+ socks_version, status_code = @socks_data.slice(0, 2).unpack('CC')
- if response_code == 0 # success
- @socks_state = :connected
+ raise SOCKSError, "SOCKS version #{socks_version} is not 5" unless socks_version == 5
+ raise SOCKSError.for_response_code(status_code) unless status_code == 0
- class << self
- remove_method :receive_data
- end
+ min_size = @socks_data[3].ord == 3 ? 5 : 4
+
+ return unless @socks_data.size >= min_size
- @callback.call
-
- else # error
- error_messages = {
- 1 => "general socks server failure",
- 2 => "connection not allowed by ruleset",
- 3 => "network unreachable",
- 4 => "host unreachable",
- 5 => "connection refused",
- 6 => "TTL expired",
- 7 => "command not supported",
- 8 => "address type not supported"
- }
-
- error_message = error_messages[response_code] || "unknown error (code: #{response_code})"
- fail "socks5 connect error: #{error_message}"
+ size = case @socks_data[3].ord
+ when 1 then 4
+ when 3 then @socks_data[4].ord
+ when 4 then 16
+ else raise SOCKSError.for_response_code(@socks_data[3])
end
+
+ return unless @socks_data.size >= min_size + size
+
+ bind_addr = @socks_data[min_size ... (min_size + size)]
+
+ socks_unhook(case @socks_data[3].ord
+ when 1 then bind_addr.bytes.to_a.join(?.)
+ when 3 then bind_addr
+ when 4 then # TODO: yeah, I'm a lazy italian
+ end)
end
end
def socks_methods
methods = []
- methods << 2 if !@username.nil? # 2 => Username/Password Authentication
+ methods << 2 if !@socks_username.nil? # 2 => Username/Password Authentication
methods << 0 # 0 => No Authentication Required
methods
end
-
+ end
end
end
View
@@ -30,4 +30,4 @@ def unbind
end
end
-end
+end

0 comments on commit 2145ac9

Please sign in to comment.