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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/Gemfile +14 -0
- data/LICENSE.txt +21 -0
- data/README.md +323 -0
- data/Rakefile +15 -0
- data/docker-compose.yml +89 -0
- data/lib/socks_handler/address_type.rb +12 -0
- data/lib/socks_handler/authentication_method.rb +12 -0
- data/lib/socks_handler/command.rb +12 -0
- data/lib/socks_handler/direct_access_rule.rb +16 -0
- data/lib/socks_handler/errors.rb +35 -0
- data/lib/socks_handler/proxy_access_rule.rb +20 -0
- data/lib/socks_handler/rule.rb +66 -0
- data/lib/socks_handler/socket_socksify.rb +27 -0
- data/lib/socks_handler/tcp.rb +106 -0
- data/lib/socks_handler/tcpsocket_socksify.rb +23 -0
- data/lib/socks_handler/udp.rb +39 -0
- data/lib/socks_handler/udpsocket.rb +108 -0
- data/lib/socks_handler/udpsocket_socksify.rb +28 -0
- data/lib/socks_handler/username_password_authenticator.rb +31 -0
- data/lib/socks_handler/version.rb +6 -0
- data/lib/socks_handler.rb +114 -0
- data/sig/defs.rbs +509 -0
- data/sig/lib/socks_handler/rule.rbs +5 -0
- data/sig/lib/socks_handler/username_password_authenticator.rbs +6 -0
- data/sig/socket.rbs +4 -0
- data/socks_handler.gemspec +31 -0
- metadata +75 -0
@@ -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,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
|