socks_handler 0.1.0

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,106 @@
1
+ # frozen_string_literal: true
2
+ require "socks_handler"
3
+ require "socks_handler/command"
4
+ require "socks_handler/direct_access_rule"
5
+ require "socks_handler/proxy_access_rule"
6
+ require "socks_handler/socket_socksify"
7
+ require "socks_handler/tcpsocket_socksify"
8
+
9
+ module SocksHandler
10
+ class TCP
11
+ class << self
12
+ include SocksHandler
13
+
14
+ # Socksifies all TCP connections created by TCPSocket.new or Socket.tcp
15
+ #
16
+ # @param rules [Array<DirectAccessRule, ProxyAccessRule>] socksify a
17
+ # socket according to the first rule whose remote host patterns match
18
+ # the remote host
19
+ # @return [nil]
20
+ #
21
+ # @example
22
+ # SocksHandler::TCP.socksify([
23
+ # # Access 127.0.0.1, ::1 or hosts that end in ".local" directly
24
+ # SocksHandler::DirectAccessRule.new(host_patterns: %w[127.0.0.1 ::1] + [/\.local\z/]),
25
+ #
26
+ # # Access hosts that end in ".ap-northeast-1.compute.internal" through 127.0.0.1:1080
27
+ # SocksHandler::ProxyAccessRule.new(
28
+ # host_patterns: [/\.ap-northeast-1\.compute\.internal\z/],
29
+ # socks_server: "127.0.0.1:1080",
30
+ # ),
31
+ #
32
+ # # Access hosts that end in ".ec2.internal" through 127.0.0.1:1081
33
+ # SocksHandler::ProxyAccessRule.new(
34
+ # host_patterns: [/\.ec2\.internal\z/],
35
+ # socks_server: "127.0.0.1:1081",
36
+ # ),
37
+ #
38
+ # # Access others hosts through 127.0.0.1:1082 with username/password auth
39
+ # SocksHandler::ProxyAccessRule.new(
40
+ # host_patterns: [//],
41
+ # socks_server: "127.0.0.1:1082",
42
+ # username: "user",
43
+ # password: ENV["SOCKS_SERVER_PASSWORD"],
44
+ # ),
45
+ # ])
46
+ def socksify(rules)
47
+ @rules = rules
48
+
49
+ unless TCPSocket.ancestors.include?(SocksHandler::TCPSocketSocksify)
50
+ TCPSocket.prepend(SocksHandler::TCPSocketSocksify)
51
+ end
52
+
53
+ unless Socket.singleton_class.ancestors.include?(SocksHandler::SocketSocksify)
54
+ Socket.singleton_class.prepend(SocksHandler::SocketSocksify)
55
+ end
56
+
57
+ nil
58
+ end
59
+
60
+ # @return [nil]
61
+ def desocksify
62
+ @rules = []
63
+ nil
64
+ end
65
+
66
+ # @param host [String] a domain name or IP address of a remote host
67
+ # @return [DirectAccessRule, ProxyAccessRule, nil]
68
+ def find_rule(host)
69
+ rules.find { |r| r.match?(host) }
70
+ end
71
+
72
+ # Connects a host through a socks server
73
+ #
74
+ # @param socket [Socket, TCPSocket] a socket that has connected to a socks server
75
+ # @param remote_host [String]
76
+ # @param remote_port [Integer, String] a port number or service name such as "http"
77
+ # @param username [String, nil]
78
+ # @param password [String, nil]
79
+ # @return [nil]
80
+ #
81
+ # @example
82
+ # socket = TCPSocket.new("127.0.0.1", 1080) # or Socket.tcp("127.0.0.1", 1080)
83
+ # # "nginx" is an HTTP server only the socks server can access
84
+ # SocksHandler::TCP.establish_connection(socket, "nginx", 80)
85
+ #
86
+ # socket.write(<<~REQUEST.gsub("\n", "\r\n"))
87
+ # HEAD / HTTP/1.1
88
+ # Host: nginx
89
+ #
90
+ # REQUEST
91
+ # puts socket.gets #=> HTTP/1.1 200 OK
92
+ def establish_connection(socket, remote_host, remote_port, username = nil, password = nil)
93
+ negotiate(socket, username, password)
94
+ send_details(socket, Command::CONNECT, remote_host, remote_port)
95
+ nil
96
+ end
97
+
98
+ private
99
+
100
+ # @return [Array<DirectAccessRule, ProxyAccessRule>]
101
+ def rules
102
+ @rules ||= []
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SocksHandler
4
+ module TCPSocketSocksify
5
+ # @param remote_host [String]
6
+ # @param remote_port [Integer, String]
7
+ # @param local_host [String, nil]
8
+ # @param local_port [Integer, String]
9
+ # @param connect_timeout [Integer, Float, nil]
10
+ def initialize(remote_host, remote_port, local_host = nil, local_port = nil, connect_timeout: nil)
11
+ rule = SocksHandler::TCP.find_rule(remote_host)
12
+ return super if rule.nil? || rule.direct
13
+
14
+ super(rule.host, rule.port, local_host, local_port, connect_timeout: connect_timeout)
15
+ begin
16
+ SocksHandler::TCP.establish_connection(self, remote_host, remote_port, rule.username, rule.password)
17
+ rescue
18
+ close
19
+ raise
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+ require "socks_handler"
3
+ require "socks_handler/command"
4
+ require "socks_handler/udpsocket"
5
+
6
+ module SocksHandler
7
+ class UDP
8
+ class << self
9
+ include SocksHandler
10
+
11
+ # Associates a TCP socket with a UDP connection
12
+ #
13
+ # @param socket [Socket, TCPSocket] a socket that has connected to a socks server
14
+ # @param bind_host [String] host for UDPSocket#bind
15
+ # @param bind_port [Integer, String] port for UDPSocket#bind
16
+ # @param username [String, nil]
17
+ # @param password [String, nil]
18
+ # @return [SocksHandler::UDPSocket]
19
+ #
20
+ # @example
21
+ # tcp_socket = TCPSocket.new("127.0.0.1", 1080) # or Socket.tcp("127.0.0.1", 1080)
22
+ # udp_socket = SocksHandler::UDP.associate_udp(tcp_socket, "0.0.0.0", 0)
23
+ #
24
+ # "echo" is a UDP echo server that only the socks server can access
25
+ # udp_socket.send("hello", 0, "echo", 7)
26
+ # puts udp_socket.gets #=> hello
27
+ def associate_udp(socket, bind_host, bind_port, username = nil, password = nil)
28
+ negotiate(socket, username, password)
29
+ address, port = send_details(socket, Command::UDP_ASSOCIATE, bind_host, bind_port)
30
+ SocksHandler::UDPSocket.new.tap do |s|
31
+ # Use peeraddr instead of address
32
+ # because we might not be able to access the address directly
33
+ s.bind(bind_host, bind_port)
34
+ s.connect_socks_server(socket.peeraddr[3], port)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,108 @@
1
+ require "socket"
2
+ require "stringio"
3
+
4
+ require "socks_handler"
5
+
6
+ module SocksHandler
7
+ class UDPSocket < ::UDPSocket
8
+ include SocksHandler
9
+
10
+ # @return [Integer]
11
+ MAX_REPLY_SIZE = 262 # When the length of the domain name is 255
12
+
13
+ # @!method connect_socks_server(host, port)
14
+ # @param host [String]
15
+ # @param port [Integer]
16
+ # @return [Integer]
17
+ alias_method :connect_socks_server, :connect
18
+
19
+
20
+ # @param host [String]
21
+ # @param port [Integer]
22
+ # @return [Integer]
23
+ # @see UDPSocket#connect
24
+ def connect(host, port)
25
+ @host = host
26
+ @port = port
27
+ 0
28
+ end
29
+
30
+ # @param maxlen [Integer]
31
+ # @param flags [Integer]
32
+ # @return [String]
33
+ # @see UDPSocket#recv
34
+ def recv(maxlen, flags = 0)
35
+ resp, _ = recvfrom(maxlen, flags)
36
+ resp
37
+ end
38
+
39
+ # @!method recvfrom_socks_server(maxlen, flags = 0)
40
+ # @param maxlen [Integer]
41
+ # @param maxlen [Integer]
42
+ # @param flags [Integer]
43
+ # @return [Array(String, Array(String, Integer, String, String))]
44
+ alias_method :recvfrom_socks_server, :recvfrom
45
+
46
+ # @param maxlen [Integer]
47
+ # @param flags [Integer]
48
+ # @return [Array(String, Array(String, Integer, String, String))]
49
+ # @see UDPSocket#recvfrom)
50
+ def recvfrom(maxlen, flags = 0)
51
+ recvfrom_via(method(:recvfrom_socks_server), maxlen + MAX_REPLY_SIZE, flags)
52
+ end
53
+
54
+ # @!method recvfrom_socks_server_nonblock(maxlen, flags = 0)
55
+ # @param maxlen [Integer]
56
+ # @param flags [Integer]
57
+ # @return [Array(String, Array(String, Integer, String, String))]
58
+ alias_method :recvfrom_socks_server_nonblock, :recvfrom_nonblock
59
+
60
+ # @param maxlen [Integer]
61
+ # @param flags [Integer]
62
+ # @return [Array(String, Array(String, Integer, String, String))]
63
+ # @see UDPSocket#recvfrom_nonblock
64
+ def recvfrom_nonblock( maxlen, flags = 0)
65
+ recvfrom_via(method(:recvfrom_socks_server_nonblock), maxlen + MAX_REPLY_SIZE, flags)
66
+ end
67
+
68
+ # @param mesg [String]
69
+ # @param flags [Integer]
70
+ # @param host [String]
71
+ # @param port [Integer, String]
72
+ # @return [Integer]
73
+ # @see UDPSocket#send
74
+ def send(mesg, flags, host = nil, port = nil)
75
+ if @host && @port && (host || port)
76
+ raise Errno::EISCONN, "Socket is already connected"
77
+ end
78
+
79
+ host ||= @host
80
+ port ||= @port
81
+ if port.nil?
82
+ if host.nil?
83
+ raise Errno::EDESTADDRREQ, "Destination address required"
84
+ else
85
+ host, port = (host.is_a?(String) ? Addrinfo.new(host) : host).getnameinfo
86
+ end
87
+ end
88
+
89
+ request = [0x00, 0x00, 0x00].pack("C*") # Currently, the fragment number is fixed to 0
90
+ request << build_destination_packets(host, port)
91
+ request << [mesg].pack("a*")
92
+ super(request, flags)
93
+ end
94
+
95
+ private
96
+
97
+ # @param method [Method]
98
+ # @param maxlen [Integer]
99
+ # @param flags [Integer]
100
+ # @return [Array(String, Array(String, Integer, String, String))]
101
+ def recvfrom_via(method, maxlen, flags)
102
+ data, inet_addr = method.call(maxlen + MAX_REPLY_SIZE, flags)
103
+ response = StringIO.new(data)
104
+ process_reply(response)
105
+ [response.read, inet_addr]
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SocksHandler
4
+ module UDPSocketSocksify
5
+ # @param host [String]
6
+ # @param port [Integer]
7
+ # @return [Integer]
8
+ def connect(host, port)
9
+ @socks_handler_host = host
10
+ @socks_handler_port = port
11
+
12
+ rule = SocksHandler::UDP.find_rule(remote_host)
13
+ return super if rule.nil? || rule.direct
14
+
15
+ socket = TCPSocket.new(rule.host, rule.port)
16
+ begin
17
+ SocksHandler::UDP.associate_udp(socket, "0.0.0.0", 0, rule.username, rule.password)
18
+ rescue
19
+ socket.close
20
+ raise
21
+ end
22
+
23
+ 0
24
+ end
25
+
26
+ def send()
27
+ end
28
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+ require "socks_handler/errors"
3
+
4
+ module SocksHandler
5
+ # An implementation of https://www.ietf.org/rfc/rfc1929.txt
6
+ class UsernamePasswordAuthenticator
7
+ # @return [Integer]
8
+ SUBNEGOTIATION_VERSION = 0x01
9
+
10
+ # @param username [String]
11
+ # @param password [String]
12
+ def initialize(username, password)
13
+ raise ArgumentError, "Username is too long" if username.bytesize > 256
14
+ raise ArgumentError, "Password is too long" if password.bytesize > 256
15
+
16
+ @username = username
17
+ @password = password
18
+ end
19
+
20
+ # @param socket [Socket, TCPSocket]
21
+ # @return [nil]
22
+ def authenticate(socket)
23
+ socket.write([SUBNEGOTIATION_VERSION, @username.bytesize, @username, @password.bytesize, @password].pack("CCa*Ca*"))
24
+ _version, status = socket.recv(2).unpack("C*")
25
+ if status != 0x00
26
+ raise AuthenticationFailure, "username: #{@username}, status: #{status}"
27
+ end
28
+ nil
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SocksHandler
4
+ # @return [String]
5
+ VERSION = "0.1.0"
6
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+ require "ipaddr"
3
+ require "socket"
4
+
5
+ require "socks_handler/address_type"
6
+ require "socks_handler/authentication_method"
7
+ require "socks_handler/errors"
8
+ require "socks_handler/tcp"
9
+ require "socks_handler/udp"
10
+ require "socks_handler/username_password_authenticator"
11
+ require "socks_handler/version"
12
+
13
+ # An implementation of https://www.ietf.org/rfc/rfc1928.txt
14
+ # @private
15
+ module SocksHandler
16
+ # @return [Integer]
17
+ PROTOCOL_VERSION = 0x05
18
+
19
+ private
20
+
21
+ # @param socket [Socket, TCPSocket]
22
+ # @param username [String, nil]
23
+ # @param password [String, nil]
24
+ # @return [nil]
25
+ def negotiate(socket, username, password)
26
+ case choose_auth_method(socket, username)
27
+ when AuthenticationMethod::NONE
28
+ # Do nothing
29
+ when AuthenticationMethod::USERNAME_PASSWORD
30
+ UsernamePasswordAuthenticator.new(username, password).authenticate(socket)
31
+ else
32
+ raise NoAcceptableMethods
33
+ end
34
+ nil
35
+ end
36
+
37
+ # @param socket [Socket, TCPSocket]
38
+ # @param username [String, nil]
39
+ # @return [Integer] code of authentication method
40
+ def choose_auth_method(socket, username)
41
+ methods = [AuthenticationMethod::NONE]
42
+ methods << AuthenticationMethod::USERNAME_PASSWORD if username
43
+ socket.write([PROTOCOL_VERSION, methods.size, *methods].pack("C*"))
44
+
45
+ version, method = socket.recv(2).unpack("C2")
46
+ if version != PROTOCOL_VERSION
47
+ raise UnsupportedProtocol, "SOCKS5 is not supported"
48
+ end
49
+
50
+ method
51
+ end
52
+
53
+ # @param socket [Socket, TCPSocket]
54
+ # @param command [Integer]
55
+ # @param remote_host [String]
56
+ # @param remote_port [Integer, String] a port number or service name such as "http"
57
+ # @return [Array(String, Integer)] an array of [bound_address, bound_port]
58
+ def send_details(socket, command, remote_host, remote_port)
59
+ request = [PROTOCOL_VERSION, command, 0x00].pack("C*")
60
+ request << build_destination_packets(remote_host, remote_port)
61
+ socket.write(request)
62
+
63
+ process_reply(socket)
64
+ end
65
+
66
+ # @param remote_host [String]
67
+ # @param remote_port [Integer, String]
68
+ # @return [String]
69
+ def build_destination_packets(remote_host, remote_port)
70
+ begin
71
+ ipaddr = IPAddr.new(remote_host)
72
+ rescue IPAddr::InvalidAddressError
73
+ # Just ignore the error because remote_host must be a domain name
74
+ end
75
+
76
+ packets = +""
77
+ case
78
+ when ipaddr.nil?
79
+ packets << [AddressType::DOMAINNAME, remote_host.size, remote_host].pack("CCa*")
80
+ when ipaddr.ipv4?
81
+ packets << [AddressType::IPV4, ipaddr.hton].pack("Ca*")
82
+ when ipaddr.ipv6?
83
+ packets << [AddressType::IPV6, ipaddr.hton].pack("Ca*")
84
+ else
85
+ raise "Unknown type of IPAddr: #{ipaddr.inspect}"
86
+ end
87
+ packets << [remote_port.is_a?(String) ? Socket.getservbyname(remote_port) : remote_port].pack("n")
88
+
89
+ packets
90
+ end
91
+
92
+ # @param io [TCPSocket, Socket, StringIO]
93
+ # @return [Array(String, Integer)] an array of [bound_address, bound_port]
94
+ def process_reply(io)
95
+ _, rep, _, atyp = io.read(4).unpack("C*")
96
+ raise RelayRequestFailure.new(rep) if rep != 0x00
97
+
98
+ case atyp
99
+ when AddressType::IPV4
100
+ bound_address = IPAddr.ntop(io.read(4))
101
+ when AddressType::DOMAINNAME
102
+ len = io.read(1).unpack("C").first
103
+ bound_address = io.read(len).unpack("a*").first
104
+ when AddressType::IPV6
105
+ bound_address = IPAddr.ntop(io.read(16))
106
+ else
107
+ raise "Unknown atyp #{atyp}"
108
+ end
109
+
110
+ bound_port = io.read(2).unpack("n").first
111
+
112
+ [bound_address, bound_port]
113
+ end
114
+ end