https://github.com/abicky/socks_handler
A flexible socksifier for sockets created by TCPSocket.new, Socket.tcp or UDPSocket.new
https://github.com/abicky/socks_handler
Last synced: about 1 year ago
JSON representation
A flexible socksifier for sockets created by TCPSocket.new, Socket.tcp or UDPSocket.new
- Host: GitHub
- URL: https://github.com/abicky/socks_handler
- Owner: abicky
- License: mit
- Created: 2023-08-27T10:26:46.000Z (almost 3 years ago)
- Default Branch: main
- Last Pushed: 2023-08-30T09:37:41.000Z (almost 3 years ago)
- Last Synced: 2025-04-24T12:04:18.874Z (about 1 year ago)
- Language: Ruby
- Homepage:
- Size: 29.3 KB
- Stars: 0
- Watchers: 2
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE.txt
Awesome Lists containing this project
README
# 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](https://www.inet.no/dante/doc/1.3.x/socksify.1.html) and [proxychains4](https://github.com/rofl0r/proxychains-ng) 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:
```yaml
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:
```ruby
require "socks_handler"
socket = TCPSocket.new("127.0.0.1", 1080) # or Socket.tcp("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 #=> HTTP/1.1 200 OK
```
If you want to access the host through the SOCKS server implicitly, you can use `SocksHandler.socksify` as follows:
```ruby
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 #=> HTTP/1.1 200 OK
```
With `SocksHandler::TCP.socksify`, other methods using `TCPSocket.new` or `Socket.tcp` also access the remote host through the SOCKS server:
```ruby
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:
```yaml
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:
```ruby
require "socks_handler"
tcp_socket = TCPSocket.new("127.0.0.1", 1080) # or Socket.tcp("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 #=> hello
```
## 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:
```yaml
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:
```ruby
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
#=> Unknown MySQL server host 'mysql' (8) (Mysql2::Error::ConnectionError)
```
## Related Work
### Gems
The following projects provide similar gems:
* [ruby-proxifier](https://github.com/samuelkadolph/ruby-proxifier)
- This project seems to no longer be maintained.
* [socksify-ruby](https://github.com/astro/socksify-ruby)
### 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 </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":
```ruby
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 '
from code.rb:4:in `each'
from code.rb:4:in `'
```
### ProxyChains-NG
[ProxyChains-NG](https://github.com/rofl0r/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:
```ruby
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
#
[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](https://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](https://opensource.org/licenses/MIT).