SocksHandler
SocksHandler is a flexible socksifier for sockets created by TCPSocket.new
, Socket.tcp
, or UDPSocket.new
that solves the following issues:
SOCKSSocket
is not easy to use
- It is unavailable unless ruby is built with
--enable-socks
, and even if it is available, we cannot use domain names that the network where the program runs cannot resolve since socket classes, including SOCKSSocket
, call getaddrinfo
at initialization.
- Famous socksifiers such as socksify and proxychains4 don't support rules using domain names
- Besides, they don't work on macOS if Ruby is managed by rbenv maybe due to SIP (System Integrity Protection)
For more details, see the section "Related Work."
Installation
Install the gem and add to the application's Gemfile by executing:
$ bundle add socks_handler
If bundler is not being used to manage dependencies, install the gem by executing:
$ gem install socks_handler
Usage
Socksify TCP Connections
Assuming that a SOCKS server that can access the host "nginx" is listening on 127.0.0.1:1080. You can prepare such an environment with the following docker-compose.yml:
version: "2.4"
services:
sockd:
image: wernight/dante
ports:
- 1080:1080
nginx:
image: nginx
Here is an example to create a socket that can access the host "nginx" from the Docker host:
require "socks_handler"
socket = TCPSocket.new("127.0.0.1", 1080)
SocksHandler::TCP.establish_connection(socket, "nginx", 80)
socket.write(<<~REQUEST.gsub("\n", "\r\n"))
HEAD / HTTP/1.1
Host: nginx
REQUEST
puts socket.gets
If you want to access the host through the SOCKS server implicitly, you can use SocksHandler.socksify
as follows:
require "socks_handler"
SocksHandler::TCP.socksify([
SocksHandler::ProxyAccessRule.new(
host_patterns: ["nginx"],
socks_server: "127.0.0.1:1080",
)
])
socket = TCPSocket.new("nginx", 80)
socket.write(<<~REQUEST.gsub("\n", "\r\n"))
HEAD / HTTP/1.1
Host: nginx
REQUEST
puts socket.gets
With SocksHandler::TCP.socksify
, other methods using TCPSocket.new
or Socket.tcp
also access the remote host through the SOCKS server:
require "net/http"
require "socks_handler"
SocksHandler::TCP.socksify([
SocksHandler::ProxyAccessRule.new(
host_patterns: ["nginx"],
socks_server: "127.0.0.1:1080",
)
])
Net::HTTP.start("nginx", 80) do |http|
pp http.head("/")
end
For more details, see the document of SocksHandler::TCP.socksify
:
$ ri SocksHandler::TCP.socksify
Socksify UDP Connections
Assuming that a SOCKS server that can access the host "echo", which is a UDP echo server, is listening on 127.0.0.1:1080. You can prepare such an environment with the following docker-compose.yml:
version: "2.4"
services:
sockd:
image: wernight/dante
ports:
- 1080:1080
- 1024-1030:1024-1030/udp
sysctls:
net.ipv4.ip_local_port_range: "1024 1030"
echo:
image: abicky/ncat:latest
command: -e /bin/cat -kul 7
init: true
Here is an example to create a socket that can access the host "nginx" from the Docker host:
require "socks_handler"
tcp_socket = TCPSocket.new("127.0.0.1", 1080)
udp_socket = SocksHandler::UDP.associate_udp(tcp_socket, "0.0.0.0", 0)
udp_socket.send("hello", 0, "echo", 7)
puts udp_socket.gets
Limitation
As SocksHandler
only socksifies TCP connections created by TCPSocket.new
or Socket.tcp
, it doesn't socksify connections created by native extensions.
For example, assuming that a SOCKS server that can access the host "mysql" is listening on 127.0.0.1:1080 as follows:
version: "2.4"
services:
sockd:
image: wernight/dante
mysql:
image: mysql:8
environment:
MYSQL_ROOT_PASSWORD: password
The following code raises Mysql2::Error::ConnectionError
because the gem "mysql2" tries to connect to the server via a native extension:
require "mysql2"
SocksHandler.socksify([
SocksHandler::ProxyAccessRule.new(
host_patterns: ["mysql"],
socks_server: "127.0.0.1:1080",
)
])
client = Mysql2::Client.new(
host: "mysql",
port: 3306,
username: "root",
password: "password",
)
client.ping
Related Work
Gems
The following projects provide similar gems:
SOCKSSocket
On macOS, you can build ruby with SOCKSSocket
as follows:
$ brew install dante bison
$ git clone https://github.com/ruby/ruby.git
$ cd ruby
$ git checkout v3_2_2
$ ./configure --enable-socks
$ PATH="/usr/local/opt/bison/bin:$PATH" make -j$(nproc) install
$ cat <<EOF >/etc/socks.conf
route {
from: 0.0.0.0/0 to: 0.0.0.0/0 via: 127.0.0.1 port = 1080
proxyprotocol: socks_v5
method: none
}
EOF
Here is example code to access nginx launched using docker-compose.yml in the section "Usage":
require "socket"
ip = `docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $(docker compose ps -q nginx)`.chomp
[ip, "nginx"].each do |host|
puts "Send an HTTP request to #{host}"
socket = SOCKSSocket.new(host, 80)
socket.write(<<~REQUEST.gsub("\n", "\r\n"))
HEAD / HTTP/1.1
Host: nginx
REQUEST
puts "Received: #{socket.gets}"
end
As you can see below, we cannot use the domain name "nginx" since socket classes, including SOCKSSocket
, call getaddrinfo
at initialization:
$ /usr/local/bin/ruby /path/to/code.rb
Send an HTTP request to 192.168.160.4
Received: HTTP/1.1 200 OK
Send an HTTP request to nginx
code.rb:6:in `initialize': getaddrinfo: nodename nor servname provided, or not known (SocketError)
from code.rb:6:in `new'
from code.rb:6:in `block in <main>'
from code.rb:4:in `each'
from code.rb:4:in `<main>'
ProxyChains-NG
ProxyChains-NG is a socksifier that works well even on macOS.
On macOS, you can install it as follows:
$ brew install proxychains-ng
Then edit /usr/local/etc/proxychains.conf
to use the socks server listening on 127.0.0.1:1080:
[ProxyList]
socks5 127.0.0.1 1080
ProxyChains-NG can socksify even connections created by native extensions. Here is example code to demonstrate it:
require "net/http"
require "mysql2"
Net::HTTP.start("nginx", 80) do |http|
pp http.head("/")
end
client = Mysql2::Client.new(
host: "mysql",
port: 3306,
username: "root",
password: "password",
)
puts client.ping
As you can see below, the program can access containers though the socks server:
$ proxychains4 /usr/local/bin/ruby /path/to/code.rb
[proxychains] config file found: /usr/local/etc/proxychains.conf
[proxychains] preloading /usr/local/Cellar/proxychains-ng/4.16/lib/libproxychains4.dylib
[proxychains] DLL init: proxychains-ng 4.16
[proxychains] Strict chain ... 127.0.0.1:1080 ... nginx:80 ... OK
#<Net::HTTPOK 200 OK readbody=true>
[proxychains] Strict chain ... 127.0.0.1:1080 ... mysql:3306 ... OK
true
However, it doesn't work if Ruby is managed by rbenv:
$ rbenv local
3.2.2
$ proxychains4 ruby /path/to/code.rb
[proxychains] config file found: /usr/local/etc/proxychains.conf
[proxychains] preloading /usr/local/Cellar/proxychains-ng/4.16/lib/libproxychains4.dylib
/Users/arabiki/.anyenv/envs/rbenv/versions/3.2.2/lib/ruby/3.2.0/net/http.rb:1271:in `initialize': Failed to open TCP connection to nginx:80 (getaddrinfo: nodename nor servname provided, or not known) (SocketError)
-- snip --
Maybe the reason is that macOS doesn't allow any system binaries to preload libraries and rbenv uses /usr/bin/env
:
$ proxychains4 env /usr/local/bin/ruby /path/to/code.rb
[proxychains] config file found: /usr/local/etc/proxychains.conf
[proxychains] preloading /usr/local/Cellar/proxychains-ng/4.16/lib/libproxychains4.dylib
/usr/local/lib/ruby/3.2.0/net/http.rb:1271:in `initialize': Failed to open TCP connection to nginx:80 (getaddrinfo: nodename nor servname provided, or not known) (SocketError)
-- snip --
Although ProxyChains-NG works well in almost all cases, it cannot use domain names to determine whether to access them through a socks proxy.
Development
After checking out the repo, run bin/setup
to install dependencies. Then, run rake spec
to run the tests. You can also run bin/console
for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
. To release a new version, update the version number in version.rb
, and then run bundle exec rake release
, which will create a git tag for the version, push git commits and the created tag, and push the .gem
file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/abicky/socks_handler.
License
The gem is available as open source under the terms of the MIT License.