class RaptorIO::Socket::Comm::SOCKS

Communication through a SOCKS proxy

@see openssh.org/txt/socks4.protocol @see tools.ietf.org/html/rfc1928

Attributes

socks_comm[RW]

@!attribute socks_comm

The {Comm} used to connect to the SOCKS server
@return [Comm]
socks_host[RW]

@!attribute socks_host

The SOCKS server's address
@return [String]
socks_port[RW]

@!attribute socks_port

The SOCKS server's port
@return [Fixnum]

Public Class Methods

new(options = {}) click to toggle source

@param options [Hash] @option options :socks_host [String,IPAddr] @option options :socks_port [Fixnum] @option options :socks_comm [Comm]

# File lib/raptor-io/socket/comm/socks.rb, line 59
def initialize(options = {})
  @socks_host = options[:socks_host]
  @socks_port = options[:socks_port].to_i
  @socks_comm = options[:socks_comm]
end

Public Instance Methods

create_tcp(options) click to toggle source

Connect to ‘:peer_host`

@option (see Comm#create_tcp)

@return [Socket::TCP]

@raise [RaptorIO::Socket::Error::ConnectTimeout]

# File lib/raptor-io/socket/comm/socks.rb, line 83
def create_tcp(options)
  @socks_socket = socks_comm.create_tcp(
    peer_host: socks_host,
    peer_port: socks_port
  )

  negotiate_connection(options[:peer_host], options[:peer_port])

  if options[:ssl_context]
    RaptorIO::Socket::TCP::SSL.new(@socks_socket, options)
  else
    RaptorIO::Socket::TCP.new(@socks_socket, options)
  end
end
support_ipv6?() click to toggle source

(see Comm#support_ipv6?)

# File lib/raptor-io/socket/comm/socks.rb, line 66
def support_ipv6?
  begin
    tcp = create_tcp("::1", {})
    tcp.close
    true
  rescue RaptorIO::Error
    nil
  end
end

Private Instance Methods

handle_reply(reply_pkt) click to toggle source
# File lib/raptor-io/socket/comm/socks.rb, line 177
def handle_reply(reply_pkt)

  # [ version ][ reply code ][ reserved ][ atyp ]
  _, reply, _, type = reply_pkt.unpack("C4")

  #  X'00' succeeded
  #  X'01' general SOCKS server failure
  #  X'02' connection not allowed by ruleset
  #  X'03' Network unreachable
  #  X'04' Host unreachable
  #  X'05' Connection refused
  #  X'06' TTL expired
  #  X'07' Command not supported
  #  X'08' Address type not supported
  #  X'09' to X'FF' unassigned
  case reply
  when ReplyCodes::SUCCEEDED
    # Read in the bind addr. The protocol spec says this is supposed
    # to be the getsockname(2) address of the sockfd on the server,
    # which isn't all that useful to begin with. SSH(1) always
    # populates it with NULL bytes, making it completely pointless.
    # Read it off the socket and ignore it so it doesn't get in the
    # way of the proxied traffic.
    case type
    when AddressTypes::ATYP_IPv4
      @socks_socket.read(4)
    when AddressTypes::ATYP_IPv6
      @socks_socket.read(16)
    when AddressTypes::ATYP_DOMAINNAME
      # Pascal string, so read in the length and then read that many
      len = @socks_socket.read(1).to_i
      @socks_socket.read(len)
    end
    # bind port
    @socks_socket.read(2)

  when ReplyCodes::NETUNREACH, ReplyCodes::HOSTUNREACH
    @socks_socket.close
    raise RaptorIO::Socket::Error::HostUnreachable
  when ReplyCodes::CONNREFUSED
    @socks_socket.close
    raise RaptorIO::Socket::Error::ConnectionRefused
  when ReplyCodes::GENERAL_FAILURE,
       ReplyCodes::NOT_ALLOWED,
       ReplyCodes::TTL_EXPIRED,
       ReplyCodes::CMD_NOT_SUPPORTED,
       ReplyCodes::ATYP_NOT_SUPPORTED
    # Then this is a kind of failure that doesn't map well to standard
    # socket errors. Just call it a ConnectionError.
    @socks_socket.close
    raise RaptorIO::Socket::Error::ConnectionError
  else
    # Then this is an unassigned error code. No idea what it is, so
    # just call it a ConnectionError
    @socks_socket.close
    raise RaptorIO::Socket::Error::ConnectionError
  end
end
negotiate_connection(peer_host, peer_port) click to toggle source

Attempt to create a connection to ‘peer_host`:`peer_port` via the SOCKS server at {#socks_host}:{#socks_port}.

@param peer_host [String] An address or hostname @param peer_port [Fixnum] TCP port to connect to

@raise [Error::ConnectionError] When the connection fails

# File lib/raptor-io/socket/comm/socks.rb, line 108
def negotiate_connection(peer_host, peer_port)
  # From RFC1928:
  # ```
  #   o  X'00' NO AUTHENTICATION REQUIRED
  #   o  X'01' GSSAPI
  #   o  X'02' USERNAME/PASSWORD
  #   o  X'03' to X'7F' IANA ASSIGNED
  #   o  X'80' to X'FE' RESERVED FOR PRIVATE METHODS
  #   o  X'FF' NO ACCEPTABLE METHODS
  # ```
  auth_methods = [ 0 ]
  # [ version ][ N methods ][ methods ... ]
  v5_pkt = [ 5, auth_methods.count, *auth_methods ].pack("CCC*")

  @socks_socket.write(v5_pkt)
  response = @socks_socket.read(2)

  case response
  when "\x05\x00".force_encoding('binary')
    # Then they accepted NO AUTHENTICATION and we can send a connect
    # request *without* a password
    request = pack_v5_connect_packet(peer_host, peer_port.to_i)
  else
    # Then they didn't like what we had to offer.
    @socks_socket.close
    raise RaptorIO::Socket::Error::ConnectionError, "Proxy connection failed"
  end

  @socks_socket.write(request)

  reply_pkt = @socks_socket.read(4)
  if reply_pkt.nil?
    # ssh(1) likes to just sever the connection if it can't connect
    raise RaptorIO::Socket::Error::ConnectionError
  end

  handle_reply(reply_pkt)
end
pack_v5_connect_packet(peer_host, peer_port) click to toggle source
# File lib/raptor-io/socket/comm/socks.rb, line 147
def pack_v5_connect_packet(peer_host, peer_port)
  begin
    ip = IPAddr.parse(peer_host)
  rescue ArgumentError
    type = AddressTypes::ATYP_DOMAINNAME
    # Packed as a Pascal string
    packed_addr = [peer_host.length, peer_host].pack("Ca*")
  else
    if ip.to_range.count != 1
      raise ArgumentError, "Invalid host"
    end
    type = if ip.ipv4?
             AddressTypes::ATYP_IPv4
           elsif ip.ipv6?
             AddressTypes::ATYP_IPv6
           end
    packed_addr = ip.hton
  end
  connect_packet = [
    5, # Version
    1, # CMD, CONNECT X'01'
    0, # reserved
    type,
    packed_addr,
    peer_port
  ].pack("CCCCa*n")

  connect_packet
end