New Upstream Snapshot - ruby-socksify

Ready changes

Summary

Merged new upstream version: 1.7.2 (was: 1.7.1+gh).

Resulting package

Built on 2023-01-19T07:18 (took 3m57s)

The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:

apt install -t fresh-snapshots ruby-socksify

Lintian Result

Diff

diff --git a/.rubocop.yml b/.rubocop.yml
new file mode 100644
index 0000000..de2774e
--- /dev/null
+++ b/.rubocop.yml
@@ -0,0 +1,6 @@
+---
+require: 'rubocop-minitest'
+
+AllCops:
+  NewCops: enable
+  TargetRubyVersion: 2.0
diff --git a/ChangeLog b/ChangeLog
index 3bddc41..3890dd6 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -66,3 +66,12 @@ SOCKSify Ruby 1.7.1
 * Send requests in one call
   For compatibility with shadowsocks <=3.0.3
   (thanks to Yilin Chen)
+
+unreleased
+SOCKSify Ruby 1.7.2
+===================
+* Ruby > 3.0 support for Net::HTTP.socks_proxy (was broken in v1.7.1 for Ruby 3.1)
+* TCPSocket patched methods #socks_server and #socks_port deprecated for Ruby > 3.0
+* Fix Socksify::debug = false
+  Previously, debug was enabled if any value was assigned to Socksify::debug
+  (thanks to Dennis Blommesteijn)
diff --git a/LICENSE b/LICENSE
index 4612460..b6f081c 100644
--- a/LICENSE
+++ b/LICENSE
@@ -38,12 +38,7 @@ You can redistribute it and/or modify it under either the terms of the GPL
        d) make other distribution arrangements with the author.
 
   4. You may modify and include the part of the software into any other
-     software (possibly commercial).  But some files in the distribution
-     are not written by the author, so that they are not under this terms.
-
-     They are gc.c(partly), utils.c(partly), regex.[ch], st.[ch] and some
-     files under the ./missing directory.  See each file for the copying
-     condition.
+     software (possibly commercial). 
 
   5. The scripts and library files supplied as input to or produced as 
      output from the software do not automatically fall under the
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..4996e49
--- /dev/null
+++ b/README.md
@@ -0,0 +1,105 @@
+[![Gem Version](https://badge.fury.io/rb/socksify.svg)](https://badge.fury.io/rb/socksify)
+[![Actions Status](https://github.com/astro/socksify-ruby/workflows/CI/badge.svg?branch=master)](https://github.com/astro/socksify-ruby/actions?query=workflow%3ACI)
+
+SOCKSify Ruby
+=============
+
+What is it?
+-----------
+
+**SOCKSify Ruby** redirects any TCP connection initiated by a Ruby script through a SOCKS5 proxy. It serves as a small drop-in alternative to [tsocks](http://tsocks.sourceforge.net/), except that it handles Ruby programs only and doesn't leak DNS queries.
+
+### How does it work?
+
+```rb
+require 'socksify/http'
+```
+This adds a new class method `Net::HTTP.socks_proxy` which takes the host and port address of a socks proxy. Once set, all requests will be routed via socks. This is acheived by patching a private method in `Net::HTTP`, as sadly Ruby no longer has native socks proxy support out of the box.
+
+Additionally, `Socksify.resolve` can be used to resolve hostnames to IPv4 addresses via SOCKS.
+
+Installation
+------------
+
+`$ gem install socksify`
+
+Usage
+-----
+
+### Redirect all TCP connections of a Ruby program
+
+Run a Ruby script with redirected TCP through a local [Tor](https://www.torproject.org/) anonymizer:
+
+`$ socksify_ruby localhost 9050 script.rb`
+
+### Explicit SOCKS usage in a Ruby program (Deprecated in Ruby 3.1 onwards)
+
+Set up SOCKS connections for a local [Tor](https://www.torproject.org/) anonymizer, TCPSockets can be used as usual:
+
+```rb
+require 'socksify'
+
+TCPSocket.socks_server = "127.0.0.1"
+TCPSocket.socks_port = 9050
+rubyforge_www = TCPSocket.new("rubyforge.org", 80)
+# => #<TCPSocket:0x...>
+```
+
+### Use Net::HTTP explicitly via SOCKS
+
+Require the additional library `socksify/http` and use the `Net::HTTP.socks_proxy` method. It is similar to `Net::HTTP.Proxy` from the Ruby standard library:
+```rb
+require 'socksify/http'
+
+uri = URI.parse('http://ipecho.net/plain')
+Net::HTTP.socks_proxy('127.0.0.1', 9050).start(uri.host, uri.port) do |http|
+  req = Net::HTTP::Get.new uri
+  resp = http.request(req)
+  puts resp.inspect
+  puts resp.body
+end
+# => #<Net::HTTPOK 200 OK readbody=true>
+# => <A tor exit node ip address>
+```
+Note that `Net::HTTP.socks_proxy` never relies on `TCPSocket.socks_server`/`socks_port`. You should either set `socks_proxy` arguments explicitly or use `Net::HTTP` directly.
+
+### Resolve addresses via SOCKS
+```rb
+Socksify.resolve("spaceboyz.net")
+# => "87.106.131.203"
+```
+### Testing and Debugging
+
+A tor proxy is required before running the tests. Install tor from your usual package manager, check it is running with `pidof tor` then run the tests with:
+
+`ruby test/test_socksify.rb` (uses minitest, `gem install minitest` if you don't have it)
+
+Colorful diagnostic messages are enabled by default via:
+```rb
+Socksify::debug = true`
+```
+Development
+-----------
+
+The [repository](https://github.com/astro/socksify-ruby/) can be checked out with:
+
+`$ git-clone git@github.com:astro/socksify-ruby.git`
+
+Send patches via pull requests. Please run `rubcop` & correct any errors first.
+
+### Further ideas
+
+*   `Resolv` replacement code, so that programs which resolve by themselves don't leak DNS queries
+*   IPv6 address support
+*   UDP as soon as [Tor](https://www.torproject.org/) supports it
+*   Perhaps using standard exceptions for better compatibility when acting as a drop-in?
+
+Author
+------
+
+*   [Stephan Maka](mailto:stephan@spaceboyz.net)
+
+License
+-------
+
+SOCKSify Ruby is distributed under the terms of the GNU General Public License version 3 (see file `COPYING`) or the Ruby License (see file `LICENSE`) at your option.
\ No newline at end of file
diff --git a/bin/socksify_ruby b/bin/socksify_ruby
index 80870be..8fb4ece 100755
--- a/bin/socksify_ruby
+++ b/bin/socksify_ruby
@@ -1,7 +1,7 @@
 #!/usr/bin/env ruby
 
 if ARGV.size < 2
-  puts "Usage: #{$0} <SOCKS host> <SOCKS port> [script args ...]"
+  puts "Usage: #{$PROGRAM_NAME} <SOCKS host> <SOCKS port> [script args ...]"
   exit
 end
 
@@ -15,4 +15,3 @@ else
   require 'irb'
   IRB.start(__FILE__)
 end
-
diff --git a/debian/changelog b/debian/changelog
index 56dd1fb..20e1c5a 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+ruby-socksify (1.7.2-1) UNRELEASED; urgency=low
+
+  * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Thu, 19 Jan 2023 07:15:53 -0000
+
 ruby-socksify (1.7.1+gh-1) unstable; urgency=medium
 
   * Team upload
diff --git a/doc/index.html b/doc/index.html
index 56c927a..5d68607 100644
--- a/doc/index.html
+++ b/doc/index.html
@@ -77,6 +77,12 @@ Socksify::proxy("127.0.0.1", 9050) {
 </pre>
       </p>
 
+      <p>
+      Please note: <b>socksify is not thread-safe</b> when used this way!
+      <code>socks_server</code> and <code>socks_port</code> are stored in class
+      <code>@@</code>-variables, and applied to all threads and fibers of application.
+      </p>
+
       <h3>Use Net::HTTP explicitly via SOCKS</h3>
       <p>
 	Require the additional library <code>socksify/http</code>
diff --git a/lib/socksify.rb b/lib/socksify.rb
index 39adced..b248be2 100644
--- a/lib/socksify.rb
+++ b/lib/socksify.rb
@@ -1,364 +1,134 @@
-#encoding: us-ascii
-=begin
-    Copyright (C) 2007 Stephan Maka <stephan@spaceboyz.net>
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU General Public License as published by
-    the Free Software Foundation, either version 3 of the License, or
-    (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU General Public License for more details.
-
-    You should have received a copy of the GNU General Public License
-    along with this program.  If not, see <http://www.gnu.org/licenses/>.
-=end
+# encoding: us-ascii
+
+# Copyright (C) 2007 Stephan Maka <stephan@spaceboyz.net>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 require 'socket'
 require 'resolv'
 require 'socksify/debug'
+require 'socksify/tcpsocket'
 
+# error class
 class SOCKSError < RuntimeError
   def initialize(msg)
-    Socksify::debug_error("#{self.class}: #{msg}")
+    Socksify.debug_error("#{self.class}: #{msg}")
     super
   end
 
+  # rubocop:disable Style/Documentation
   class ServerFailure < SOCKSError
     def initialize
-      super("general SOCKS server failure")
+      super('general SOCKS server failure')
     end
   end
+
   class NotAllowed < SOCKSError
     def initialize
-      super("connection not allowed by ruleset")
+      super('connection not allowed by ruleset')
     end
   end
+
   class NetworkUnreachable < SOCKSError
     def initialize
-      super("Network unreachable")
+      super('Network unreachable')
     end
   end
+
   class HostUnreachable < SOCKSError
     def initialize
-      super("Host unreachable")
+      super('Host unreachable')
     end
   end
+
   class ConnectionRefused < SOCKSError
     def initialize
-      super("Connection refused")
+      super('Connection refused')
     end
   end
+
   class TTLExpired < SOCKSError
     def initialize
-      super("TTL expired")
+      super('TTL expired')
     end
   end
+
   class CommandNotSupported < SOCKSError
     def initialize
-      super("Command not supported")
+      super('Command not supported')
     end
   end
+
   class AddressTypeNotSupported < SOCKSError
     def initialize
-      super("Address type not supported")
-    end
-  end
-
-  def self.for_response_code(code)
-    case code
-    when 1
-      ServerFailure
-    when 2
-      NotAllowed
-    when 3
-      NetworkUnreachable
-    when 4
-      HostUnreachable
-    when 5
-      ConnectionRefused
-    when 6
-      TTLExpired
-    when 7
-      CommandNotSupported
-    when 8
-      AddressTypeNotSupported
-    else
-      self
-    end
-  end
-end
-
-class TCPSocket
-  @@socks_version ||= "5"
-  
-  def self.socks_version
-    (@@socks_version == "4a" or @@socks_version == "4") ? "\004" : "\005"
-  end
-  def self.socks_version=(version)
-    @@socks_version = version.to_s
-  end
-  def self.socks_server
-    @@socks_server ||= nil
-  end
-  def self.socks_server=(host)
-    @@socks_server = host
-  end
-  def self.socks_port
-    @@socks_port ||= nil
-  end
-  def self.socks_port=(port)
-    @@socks_port = port
-  end
-  def self.socks_username
-    @@socks_username ||= nil
-  end
-  def self.socks_username=(username)
-    @@socks_username = username
-  end
-  def self.socks_password
-    @@socks_password ||= nil
-  end
-  def self.socks_password=(password)
-    @@socks_password = password
-  end
-  def self.socks_ignores
-    @@socks_ignores ||= %w(localhost)
-  end
-  def self.socks_ignores=(ignores)
-    @@socks_ignores = ignores
-  end
-
-  class SOCKSConnectionPeerAddress < String
-    attr_reader :socks_server, :socks_port
-
-    def initialize(socks_server, socks_port, peer_host)
-      @socks_server, @socks_port = socks_server, socks_port
-      super peer_host
+      super('Address type not supported')
     end
-
-    def inspect
-      "#{to_s} (via #{@socks_server}:#{@socks_port})"
-    end
-
-    def peer_host
-      to_s
-    end
-  end
-
-  alias :initialize_tcp :initialize
-
-  # See http://tools.ietf.org/html/rfc1928
-  def initialize(host=nil, port=0, local_host=nil, local_port=nil)
-    if host.is_a?(SOCKSConnectionPeerAddress)
-      socks_peer = host
-      socks_server = socks_peer.socks_server
-      socks_port = socks_peer.socks_port
-      socks_ignores = []
-      host = socks_peer.peer_host
-    else
-      socks_server = self.class.socks_server
-      socks_port = self.class.socks_port
-      socks_ignores = self.class.socks_ignores
-    end
-
-    if socks_server and socks_port and not socks_ignores.include?(host)
-      Socksify::debug_notice "Connecting to SOCKS server #{socks_server}:#{socks_port}"
-      initialize_tcp socks_server, socks_port
-
-      socks_authenticate unless @@socks_version =~ /^4/
-
-      if host
-        socks_connect(host, port)
-      end
-    else
-      Socksify::debug_notice "Connecting directly to #{host}:#{port}"
-      initialize_tcp host, port, local_host, local_port
-      Socksify::debug_debug "Connected to #{host}:#{port}"
-    end
-  end
-
-  # Authentication
-  def socks_authenticate
-    if self.class.socks_username || self.class.socks_password
-      Socksify::debug_debug "Sending username/password authentication"
-      write "\005\001\002"
-    else
-      Socksify::debug_debug "Sending no authentication"
-      write "\005\001\000"
-    end
-    Socksify::debug_debug "Waiting for authentication reply"
-    auth_reply = recv(2)
-    if auth_reply.empty?
-      raise SOCKSError.new("Server doesn't reply authentication")
-    end
-    if auth_reply[0..0] != "\004" and auth_reply[0..0] != "\005"
-      raise SOCKSError.new("SOCKS version #{auth_reply[0..0]} not supported")
-    end
-    if self.class.socks_username || self.class.socks_password
-      if auth_reply[1..1] != "\002"
-        raise SOCKSError.new("SOCKS authentication method #{auth_reply[1..1]} neither requested nor supported")
-      end
-      auth = "\001"
-      auth += self.class.socks_username.to_s.length.chr
-      auth += self.class.socks_username.to_s
-      auth += self.class.socks_password.to_s.length.chr
-      auth += self.class.socks_password.to_s
-      write auth
-      auth_reply = recv(2)
-      if auth_reply[1..1] != "\000"
-        raise SOCKSError.new("SOCKS authentication failed")
-      end
-    else
-      if auth_reply[1..1] != "\000"
-        raise SOCKSError.new("SOCKS authentication method #{auth_reply[1..1]} neither requested nor supported")
-      end
-    end
-  end
-
-  # Connect
-  def socks_connect(host, port)
-    port = Socket.getservbyname(port) if port.is_a?(String)
-    req = String.new
-    Socksify::debug_debug "Sending destination address"
-    req << TCPSocket.socks_version
-    Socksify::debug_debug TCPSocket.socks_version.unpack "H*"
-    req << "\001"
-    req << "\000" if @@socks_version == "5"
-    req << [port].pack('n') if @@socks_version =~ /^4/
-
-    if @@socks_version == "4"
-      host = Resolv::DNS.new.getaddress(host).to_s
-    end
-    Socksify::debug_debug host
-    if host =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/  # to IPv4 address
-      req << "\001" if @@socks_version == "5"
-      _ip = [$1.to_i,
-             $2.to_i,
-             $3.to_i,
-             $4.to_i
-            ].pack('CCCC')
-      req << _ip
-    elsif host =~ /^[:0-9a-f]+$/  # to IPv6 address
-      raise "TCP/IPv6 over SOCKS is not yet supported (inet_pton missing in Ruby & not supported by Tor"
-      req << "\004"
-    else                          # to hostname
-      if @@socks_version == "5"
-        req << "\003" + [host.size].pack('C') + host
-      else
-        req << "\000\000\000\001"
-        req << "\007\000"
-        Socksify::debug_notice host
-        req << host
-        req << "\000"
-      end
-    end
-    req << [port].pack('n') if @@socks_version == "5"
-    write req
-
-    socks_receive_reply
-    Socksify::debug_notice "Connected to #{host}:#{port} over SOCKS"
   end
+  # rubocop:enable Style/Documentation
 
-  # returns [bind_addr: String, bind_port: Fixnum]
-  def socks_receive_reply
-    Socksify::debug_debug "Waiting for SOCKS reply"
-    if @@socks_version == "5"
-      connect_reply = recv(4)
-      if connect_reply.empty?
-        raise SOCKSError.new("Server doesn't reply")
-      end
-      Socksify::debug_debug connect_reply.unpack "H*"
-      if connect_reply[0..0] != "\005"
-        raise SOCKSError.new("SOCKS version #{connect_reply[0..0]} is not 5")
-      end
-      if connect_reply[1..1] != "\000"
-        raise SOCKSError.for_response_code(connect_reply.bytes.to_a[1])
-      end
-      Socksify::debug_debug "Waiting for bind_addr"
-      bind_addr_len = case connect_reply[3..3]
-                      when "\001"
-                        4
-                      when "\003"
-                        recv(1).bytes.first
-                      when "\004"
-                        16
-                      else
-                        raise SOCKSError.for_response_code(connect_reply.bytes.to_a[3])
-                      end
-      bind_addr_s = recv(bind_addr_len)
-      bind_addr = case connect_reply[3..3]
-                  when "\001"
-                    bind_addr_s.bytes.to_a.join('.')
-                  when "\003"
-                    bind_addr_s
-                  when "\004"  # Untested!
-                    i = 0
-                    ip6 = ""
-                    bind_addr_s.each_byte do |b|
-                      if i > 0 and i % 2 == 0
-                        ip6 += ":"
-                      end
-                      i += 1
+  RESPONSE_CODE_CLASSES = { 1 => ServerFailure,
+                            2 => NotAllowed,
+                            3 => NetworkUnreachable,
+                            4 => HostUnreachable,
+                            5 => ConnectionRefused,
+                            6 => TTLExpired,
+                            7 => CommandNotSupported,
+                            8 => AddressTypeNotSupported }.freeze
 
-                      ip6 += b.to_s(16).rjust(2, '0')
-                    end
-                  end
-      bind_port = recv(bind_addr_len + 2)
-      [bind_addr, bind_port.unpack('n')]
-    else
-      connect_reply = recv(8)
-      unless connect_reply[0] == "\000" and connect_reply[1] == "\x5A"
-        Socksify::debug_debug connect_reply.unpack 'H'
-        raise SOCKSError.new("Failed while connecting througth socks")
-      end
-    end
+  def self.for_response_code(code)
+    (resp = RESPONSE_CODE_CLASSES[code]) ? resp : self
   end
 end
 
+# namespace
 module Socksify
   def self.resolve(host)
-    s = TCPSocket.new
-
-    begin
-      req = String.new
-      Socksify::debug_debug "Sending hostname to resolve: #{host}"
-      req << "\005"
-      if host =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/  # to IPv4 address
-        req << "\xF1\000\001" + [$1.to_i,
-                                  $2.to_i,
-                                  $3.to_i,
-                                  $4.to_i
-                                 ].pack('CCCC')
-      elsif host =~ /^[:0-9a-f]+$/  # to IPv6 address
-        raise "TCP/IPv6 over SOCKS is not yet supported (inet_pton missing in Ruby & not supported by Tor"
-        req << "\004"
-      else                          # to hostname
-        req << "\xF0\000\003" + [host.size].pack('C') + host
-      end
-      req << [0].pack('n')  # Port
-      s.write req
-      
-      addr, _port = s.socks_receive_reply
-      Socksify::debug_notice "Resolved #{host} as #{addr} over SOCKS"
-      addr
-    ensure
-      s.close
-    end
+    socket = TCPSocket.new # no args?
+    Socksify.debug_debug "Sending hostname to resolve: #{host}"
+    req = request(host)
+    socket.write req
+    addr, _port = socket.socks_receive_reply
+    Socksify.debug_notice "Resolved #{host} as #{addr} over SOCKS"
+    addr
+  ensure
+    socket.close
   end
 
   def self.proxy(server, port)
-    default_server = TCPSocket::socks_server
-    default_port = TCPSocket::socks_port
+    default_server = TCPSocket.socks_server
+    default_port = TCPSocket.socks_port
     begin
-      TCPSocket::socks_server = server
-      TCPSocket::socks_port = port
+      TCPSocket.socks_server = server
+      TCPSocket.socks_port = port
       yield
-    ensure
-      TCPSocket::socks_server = default_server
-      TCPSocket::socks_port = default_port
+    ensure # failback
+      TCPSocket.socks_server = default_server
+      TCPSocket.socks_port = default_port
+    end
+  end
+
+  def self.request(host)
+    req = String.new << "\005"
+    case host
+    when /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/ # to IPv4 address
+      req << "\xF1\000\001#{(1..4).map { |i| Regexp.last_match(i).to_i }.pack('CCCC')}"
+    when /^[:0-9a-f]+$/ # to IPv6 address
+      raise 'TCP/IPv6 over SOCKS is not yet supported (inet_pton missing in Ruby & not supported by Tor)'
+    #   # req << "\004" # UNREACHABLE
+    else # to hostname
+      req << "\xF0\000\003#{[host.size].pack('C')}#{host}"
     end
+    req << [0].pack('n') # Port
   end
 end
diff --git a/lib/socksify/debug.rb b/lib/socksify/debug.rb
index 2ae8dc9..43d56cd 100644
--- a/lib/socksify/debug.rb
+++ b/lib/socksify/debug.rb
@@ -1,32 +1,47 @@
+# namespace
 module Socksify
+  # rubocop:disable Style/Documentation
   class Color
     class Reset
-      def self::to_s
+      def self.to_s
         "\e[0m\e[37m"
       end
     end
-  
+
     class Red < Color
-      def num; 31; end
+      def num
+        31
+      end
     end
+
     class Green < Color
-      def num; 32; end
+      def num
+        32
+      end
     end
+
     class Yellow < Color
-      def num; 33; end
+      def num
+        33
+      end
     end
-  
-    def self::to_s
+    # rubocop:enable Style/Documentation
+
+    def self.to_s
       new.to_s
     end
-  
+
+    def num
+      0
+    end
+
     def to_s
       "\e[1m\e[#{num}m"
     end
   end
 
   def self.debug=(enabled)
-    @@debug = enabled
+    @debug = enabled
   end
 
   def self.debug_debug(str)
@@ -41,12 +56,8 @@ module Socksify
     debug(Color::Red, str)
   end
 
-  private
-
   def self.debug(color, str)
-    if defined? @@debug
-      puts "#{color}#{now_s}#{Color::Reset} #{str}"
-    end
+    puts "#{color}#{now_s}#{Color::Reset} #{str}" if defined?(@debug) && @debug
   end
 
   def self.now_s
diff --git a/lib/socksify/http.rb b/lib/socksify/http.rb
index 25b63d7..73eea48 100644
--- a/lib/socksify/http.rb
+++ b/lib/socksify/http.rb
@@ -1,59 +1,53 @@
-=begin
-    Copyright (C) 2007 Stephan Maka <stephan@spaceboyz.net>
-    Copyright (C) 2011 Musy Bite <musybite@gmail.com>
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU General Public License as published by
-    the Free Software Foundation, either version 3 of the License, or
-    (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU General Public License for more details.
-
-    You should have received a copy of the GNU General Public License
-    along with this program.  If not, see <http://www.gnu.org/licenses/>.
-=end
+#  Copyright (C) 2007 Stephan Maka <stephan@spaceboyz.net>
+#  Copyright (C) 2011 Musy Bite <musybite@gmail.com>
+#
+#  This program is free software: you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation, either version 3 of the License, or
+#  (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 require 'socksify'
 require 'net/http'
+require_relative 'ruby3net_http_connectable'
 
 module Net
+  # patched class
   class HTTP
-    def self.SOCKSProxy(p_host, p_port)
-      delta = SOCKSProxyDelta
+    def self.socks_proxy(p_host, p_port)
       proxyclass = Class.new(self)
-      proxyclass.send(:include, delta)
-      proxyclass.module_eval {
-        include delta::InstanceMethods
-        extend delta::ClassMethods
+      proxyclass.send(:include, SOCKSProxyDelta)
+      proxyclass.module_eval do
+        include Ruby3NetHTTPConnectable if RUBY_VERSION.to_f > 3.0 # patch #connect method
+        include SOCKSProxyDelta::InstanceMethods
+        extend SOCKSProxyDelta::ClassMethods
         @socks_server = p_host
         @socks_port = p_port
-      }
+      end
       proxyclass
     end
 
+    class << self
+      alias SOCKSProxy socks_proxy # legacy support for non snake case method name
+    end
+
     module SOCKSProxyDelta
+      # class methods
       module ClassMethods
-        def socks_server
-          @socks_server
-        end
-
-        def socks_port
-          @socks_port
-        end
+        attr_reader :socks_server, :socks_port
       end
 
+      # instance methods - no long supports Ruby < 2
       module InstanceMethods
-        if RUBY_VERSION[0..0] >= '2'
-          def address
-            TCPSocket::SOCKSConnectionPeerAddress.new(self.class.socks_server, self.class.socks_port, @address)
-          end
-        else
-          def conn_address
-            TCPSocket::SOCKSConnectionPeerAddress.new(self.class.socks_server, self.class.socks_port, address())
-          end
+        def address
+          TCPSocket::SOCKSConnectionPeerAddress.new(self.class.socks_server, self.class.socks_port, @address)
         end
       end
     end
diff --git a/lib/socksify/ruby3net_http_connectable.rb b/lib/socksify/ruby3net_http_connectable.rb
new file mode 100644
index 0000000..4285075
--- /dev/null
+++ b/lib/socksify/ruby3net_http_connectable.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+# Ruby 3.0 private method Net::HTTP#connect
+module Ruby3NetHTTPConnectable
+  # rubocop:disable all - CAN'T LINT RUBY SOURCE CODE!
+  def connect
+    if use_ssl? # 3.1 - MOVED FROM FURTHER DOWN IN METHOD
+      # reference early to load OpenSSL before connecting,
+      # as OpenSSL may take time to load.
+      @ssl_context = OpenSSL::SSL::SSLContext.new
+    end
+
+    if proxy? then
+      conn_addr = proxy_address
+      conn_port = proxy_port
+    else
+      conn_addr = conn_address
+      conn_port = port
+    end
+
+    D "opening connection to #{conn_addr}:#{conn_port}..."
+    ######## RUBY < 3.1 ########
+    s = Timeout.timeout(@open_timeout, Net::OpenTimeout) {
+      begin
+        TCPSocket.open(conn_addr, conn_port, @local_host, @local_port)
+      rescue => e
+        raise e, "Failed to open TCP connection to " +
+          "#{conn_addr}:#{conn_port} (#{e.message})"
+      end
+    }
+    ######## END RUBY < 3.1 ########
+    s.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
+    D "opened"
+    if use_ssl?
+      if proxy?
+        plain_sock = Net::BufferedIO.new(s, read_timeout: @read_timeout, # 3.1 - FULLY QUALIFY CLASS
+                                            write_timeout: @write_timeout,
+                                            continue_timeout: @continue_timeout,
+                                            debug_output: @debug_output)
+        buf = "CONNECT #{conn_address}:#{@port} HTTP/#{Net::HTTP::HTTPVersion}\r\n" # 3.1 - FULLY QUALIFY CONSTANT
+        buf << "Host: #{@address}:#{@port}\r\n"
+        if proxy_user
+          credential = ["#{proxy_user}:#{proxy_pass}"].pack('m0')
+          buf << "Proxy-Authorization: Basic #{credential}\r\n"
+        end
+        buf << "\r\n"
+        plain_sock.write(buf)
+        Net::HTTPResponse.read_new(plain_sock).value # 3.1 - FULLY QUALIFY CLASS
+        # assuming nothing left in buffers after successful CONNECT response
+      end
+
+      ssl_parameters = Hash.new
+      iv_list = instance_variables
+      Net::HTTP::SSL_IVNAMES.each_with_index do |ivname, i| # 3.1 - FULLY QUALIFY CONSTANT
+        if iv_list.include?(ivname)
+          value = instance_variable_get(ivname)
+          unless value.nil?
+            ssl_parameters[Net::HTTP::SSL_ATTRIBUTES[i]] = value # 3.1 - FULLY QUALIFY CONSTANT
+          end
+        end
+      end
+      # @ssl_context = OpenSSL::SSL::SSLContext.new # 3.1 - MOVED TO TOP OF METHOD
+      @ssl_context.set_params(ssl_parameters)
+      @ssl_context.session_cache_mode =
+        OpenSSL::SSL::SSLContext::SESSION_CACHE_CLIENT |
+        OpenSSL::SSL::SSLContext::SESSION_CACHE_NO_INTERNAL_STORE
+      @ssl_context.session_new_cb = proc {|sock, sess| @ssl_session = sess }
+      D "starting SSL for #{conn_addr}:#{conn_port}..."
+      s = OpenSSL::SSL::SSLSocket.new(s, @ssl_context)
+      s.sync_close = true
+      # Server Name Indication (SNI) RFC 3546
+      s.hostname = @address if s.respond_to? :hostname=
+      if @ssl_session and
+         Process.clock_gettime(Process::CLOCK_REALTIME) < @ssl_session.time.to_f + @ssl_session.timeout
+        s.session = @ssl_session
+      end
+      ssl_socket_connect(s, @open_timeout)
+      if (@ssl_context.verify_mode != OpenSSL::SSL::VERIFY_NONE) && @ssl_context.verify_hostname
+        s.post_connection_check(@address)
+      end
+      D "SSL established, protocol: #{s.ssl_version}, cipher: #{s.cipher[0]}"
+    end
+    @socket = Net::BufferedIO.new(s, read_timeout: @read_timeout, # 3.1 - FULLY QUALIFY CLASS
+                                     write_timeout: @write_timeout,
+                                     continue_timeout: @continue_timeout,
+                                     debug_output: @debug_output)
+    @last_communicated = nil # 3.1 - NEW
+    on_connect
+  rescue => exception
+    if s
+      D "Conn close because of connect error #{exception}"
+      s.close
+    end
+    raise
+  end
+  # rubocop:enable all
+  private :connect
+end
diff --git a/lib/socksify/socksproxyable.rb b/lib/socksify/socksproxyable.rb
new file mode 100644
index 0000000..d79f54f
--- /dev/null
+++ b/lib/socksify/socksproxyable.rb
@@ -0,0 +1,151 @@
+# frozen_string_literal: true
+
+# decorator methods for socks proxying
+module Socksproxyable
+  # class methods
+  module ClassMethods
+    attr_accessor :socks_server, :socks_port, :socks_username, :socks_password
+
+    def socks_version
+      @socks_version ||= '5'
+    end
+
+    def socks_ignores
+      @socks_ignores ||= %w[localhost]
+    end
+
+    def socks_ignores=(*hosts)
+      @socks_ignores = hosts
+    end
+
+    def socks_version_hex
+      socks_version == '4a' || socks_version == '4' ? "\004" : "\005"
+    end
+  end
+
+  # instance method #socks_authenticate
+  module InstanceMethodsAuthenticate
+    # rubocop:disable Metrics
+    def socks_authenticate
+      if self.class.socks_username || self.class.socks_password
+        Socksify.debug_debug 'Sending username/password authentication'
+        write "\005\001\002"
+      else
+        Socksify.debug_debug 'Sending no authentication'
+        write "\005\001\000"
+      end
+      Socksify.debug_debug 'Waiting for authentication reply'
+      auth_reply = recv(2)
+      raise SOCKSError, "Server doesn't reply authentication" if auth_reply.empty?
+
+      if auth_reply[0..0] != "\004" && auth_reply[0..0] != "\005"
+        raise SOCKSError, "SOCKS version #{auth_reply[0..0]} not supported"
+      end
+
+      if self.class.socks_username || self.class.socks_password
+        if auth_reply[1..1] != "\002"
+          raise SOCKSError, "SOCKS authentication method #{auth_reply[1..1]} neither requested nor supported"
+        end
+
+        auth = "\001"
+        auth += self.class.socks_username.to_s.length.chr
+        auth += self.class.socks_username.to_s
+        auth += self.class.socks_password.to_s.length.chr
+        auth += self.class.socks_password.to_s
+        write auth
+        auth_reply = recv(2)
+        raise SOCKSError, 'SOCKS authentication failed' if auth_reply[1..1] != "\000"
+      elsif auth_reply[1..1] != "\000"
+        raise SOCKSError, "SOCKS authentication method #{auth_reply[1..1]} neither requested nor supported"
+      end
+    end
+    # rubocop:enable Metrics
+  end
+
+  # instance methods #socks_connect & #socks_receive_reply
+  module InstanceMethodsConnect
+    # rubocop:disable Metrics
+    def socks_connect(host, port)
+      port = Socket.getservbyname(port) if port.is_a?(String)
+      req = String.new
+      Socksify.debug_debug 'Sending destination address'
+      req << TCPSocket.socks_version_hex
+      Socksify.debug_debug TCPSocket.socks_version_hex.unpack 'H*'
+      req << "\001"
+      req << "\000" if self.class.socks_version == '5'
+      req << [port].pack('n') if self.class.socks_version =~ /^4/
+      host = Resolv::DNS.new.getaddress(host).to_s if self.class.socks_version == '4'
+      Socksify.debug_debug host
+      if host =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/ # to IPv4 address
+        req << "\001" if self.class.socks_version == '5'
+        ip = (1..4).map { |i| Regexp.last_match(i).to_i }.pack('CCCC')
+        req << ip
+      elsif host =~ /^[:0-9a-f]+$/ # to IPv6 address
+        raise 'TCP/IPv6 over SOCKS is not yet supported (inet_pton missing in Ruby & not supported by Tor'
+        # req << "\004" # UNREACHABLE
+      elsif self.class.socks_version == '5' # to hostname
+        # req << "\003" + [host.size].pack('C') + host
+        req << "\003#{[host.size].pack('C')}#{host}"
+      else
+        req << "\000\000\000\001" << "\007\000"
+        Socksify.debug_notice host
+        req << host << "\000"
+      end
+      req << [port].pack('n') if self.class.socks_version == '5'
+      write req
+      socks_receive_reply
+      Socksify.debug_notice "Connected to #{host}:#{port} over SOCKS"
+    end
+    # rubocop:enable Metrics
+
+    # returns [bind_addr: String, bind_port: Fixnum]
+    # rubocop:disable Metrics
+    def socks_receive_reply
+      Socksify.debug_debug 'Waiting for SOCKS reply'
+      if self.class.socks_version == '5'
+        connect_reply = recv(4)
+        raise SOCKSError, "Server doesn't reply" if connect_reply.empty?
+
+        Socksify.debug_debug connect_reply.unpack 'H*'
+        raise SOCKSError, "SOCKS version #{connect_reply[0..0]} is not 5" if connect_reply[0..0] != "\005"
+        raise SOCKSError.for_response_code(connect_reply.bytes.to_a[1]) if connect_reply[1..1] != "\000"
+
+        Socksify.debug_debug 'Waiting for bind_addr'
+        bind_addr_len = case connect_reply[3..3]
+                        when "\001"
+                          4
+                        when "\003"
+                          recv(1).bytes.first
+                        when "\004"
+                          16
+                        else
+                          raise SOCKSError.for_response_code(connect_reply.bytes.to_a[3])
+                        end
+        bind_addr_s = recv(bind_addr_len)
+        bind_addr = case connect_reply[3..3]
+                    when "\001"
+                      bind_addr_s.bytes.to_a.join('.')
+                    when "\003"
+                      bind_addr_s
+                    when "\004" # Untested!
+                      i = 0
+                      ip6 = ''
+                      bind_addr_s.each_byte do |b|
+                        ip6 += ':' if i > 0 && i.even?
+                        i += 1
+                        ip6 += b.to_s(16).rjust(2, '0')
+                      end
+                    end
+        bind_port = recv(bind_addr_len + 2)
+        [bind_addr, bind_port.unpack('n')]
+      else
+        connect_reply = recv(8)
+        unless connect_reply[0] == "\000" && connect_reply[1] == "\x5A"
+          Socksify.debug_debug connect_reply.unpack 'H'
+          raise SOCKSError, 'Failed while connecting througth socks'
+        end
+      end
+    end
+    # rubocop:enable Metrics
+  end
+end
diff --git a/lib/socksify/tcpsocket.rb b/lib/socksify/tcpsocket.rb
new file mode 100644
index 0000000..24efc6c
--- /dev/null
+++ b/lib/socksify/tcpsocket.rb
@@ -0,0 +1,72 @@
+require_relative 'socksproxyable'
+
+# monkey patch
+class TCPSocket
+  extend Socksproxyable::ClassMethods
+  include Socksproxyable::InstanceMethodsAuthenticate
+  include Socksproxyable::InstanceMethodsConnect
+
+  alias initialize_tcp initialize
+
+  # See http://tools.ietf.org/html/rfc1928
+  # rubocop:disable Metrics/ParameterLists
+  def initialize(host = nil, port = nil, local_host = nil, local_port = nil)
+    socks_peer = host if host.is_a?(SOCKSConnectionPeerAddress)
+    socks_server = set_socks_server(socks_peer)
+    socks_port = set_socks_port(socks_peer)
+    socks_ignores = set_socks_ignores(socks_peer)
+    host = socks_peer.peer_host if socks_peer
+    if socks_server && socks_port && !socks_ignores.include?(host)
+      make_socks_connection(host, port, socks_server, socks_port)
+    else
+      make_direct_connection(host, port, local_host, local_port)
+    end
+  end
+  # rubocop:enable Metrics/ParameterLists
+
+  # string representation of the peer host address
+  class SOCKSConnectionPeerAddress < String
+    attr_reader :socks_server, :socks_port
+
+    def initialize(socks_server, socks_port, peer_host)
+      @socks_server = socks_server
+      @socks_port = socks_port
+      super peer_host
+    end
+
+    def inspect
+      "#{self} (via #{@socks_server}:#{@socks_port})"
+    end
+
+    def peer_host
+      to_s
+    end
+  end
+
+  private
+
+  def set_socks_server(socks_peer = nil)
+    socks_peer ? socks_peer.socks_server : self.class.socks_server
+  end
+
+  def set_socks_port(socks_peer = nil)
+    socks_peer ? socks_peer.socks_port : self.class.socks_port
+  end
+
+  def set_socks_ignores(socks_peer = nil)
+    socks_peer ? [] : self.class.socks_ignores
+  end
+
+  def make_socks_connection(host, port, socks_server, socks_port)
+    Socksify.debug_notice "Connecting to SOCKS server #{socks_server}:#{socks_port}"
+    initialize_tcp socks_server, socks_port
+    socks_authenticate unless @socks_version =~ /^4/
+    socks_connect(host, port) if host
+  end
+
+  def make_direct_connection(host, port, local_host, local_port)
+    Socksify.debug_notice "Connecting directly to #{host}:#{port}"
+    initialize_tcp host, port, local_host, local_port
+    Socksify.debug_debug "Connected to #{host}:#{port}"
+  end
+end
diff --git a/socksify.gemspec b/socksify.gemspec
index 23d54bd..32fcedb 100644
--- a/socksify.gemspec
+++ b/socksify.gemspec
@@ -1,23 +1,27 @@
-#!/usr/bin/env ruby
+# frozen_string_literal: true
 
 require 'rubygems'
 
-spec = Gem::Specification.new do |s|
+# rubocop:disable Gemspec/RequireMFA
+Gem::Specification.new do |s|
   s.name = 'socksify'
-  s.version = "1.7.1"
-  s.summary = "Redirect all TCPSockets through a SOCKS5 proxy"
-  s.authors = ["Stephan Maka", "Andrey Kouznetsov", "Christopher Thorpe", "Musy Bite", "Yuichi Tateno", "David Dollar"]
+  s.version = '1.7.2'
+  s.summary = 'Redirect all TCPSockets through a SOCKS5 proxy'
+  s.authors = ['Stephan Maka', 'Andrey Kouznetsov', 'Christopher Thorpe', 'Musy Bite', 'Yuichi Tateno', 'David Dollar']
   s.licenses = ['Ruby', 'GPL-3.0']
-  s.email = "stephan@spaceboyz.net"
-  s.homepage = "http://socksify.rubyforge.org/"
-  s.rubyforge_project = 'socksify'
-  s.files = %w{COPYING}
-  s.files += Dir.glob("lib/**/*")
-  s.files += Dir.glob("bin/**/*")
-  s.files += Dir.glob("doc/**/*")
-  s.files = s.files.delete_if { |f| f =~ /\~$/ }
+  s.required_ruby_version = '>= 2.0'
+  s.email = 'stephan@spaceboyz.net'
+  s.homepage = 'https://github.com/astro/socksify-ruby'
+  s.files = %w[COPYING]
+  s.files += Dir.glob('lib/**/*')
+  s.files += Dir.glob('bin/**/*')
+  s.files += Dir.glob('doc/**/*')
+  # s.files = s.files.delete_if { |f| f =~ /~$/ } # ?
   s.require_path = 'lib'
-  s.executables = %w{socksify_ruby}
-  s.has_rdoc = false
-  s.extra_rdoc_files = Dir.glob("doc/**/*") + %w{COPYING}
+  s.executables = %w[socksify_ruby]
+  s.extra_rdoc_files = Dir.glob('doc/**/*') + %w[COPYING]
+  s.add_development_dependency 'minitest', '~> 5.16'
+  s.add_development_dependency 'rubocop', '~> 1.31'
+  s.add_development_dependency 'rubocop-minitest', '~> 0.20'
 end
+# rubocop:enable Gemspec/RequireMFA
diff --git a/test/tc_socksify.rb b/test/tc_socksify.rb
deleted file mode 100644
index 1612bcb..0000000
--- a/test/tc_socksify.rb
+++ /dev/null
@@ -1,215 +0,0 @@
-#!/usr/bin/ruby
-
-require 'test/unit'
-require 'net/http'
-require 'uri'
-
-$:.unshift "#{File::dirname($0)}/../lib/"
-require 'socksify'
-require 'socksify/http'
-
-
-class SocksifyTest < Test::Unit::TestCase
-  def setup
-    Socksify::debug = true
-  end
-
-  def disable_socks
-    TCPSocket.socks_server = nil
-    TCPSocket.socks_port = nil
-  end
-  def enable_socks
-    TCPSocket.socks_server = "127.0.0.1"
-    TCPSocket.socks_port = 9050
-  end
-
-  def http_tor_proxy
-    Net::HTTP::SOCKSProxy("127.0.0.1", 9050)
-  end
-
-  def test_check_tor
-    disable_socks
-
-    is_tor_direct, ip_direct = check_tor
-    assert_equal(false, is_tor_direct)
-
-    enable_socks
-
-    is_tor_socks, ip_socks = check_tor
-    assert_equal(true, is_tor_socks)
-
-    assert(ip_direct != ip_socks)
-  end
-
-  def test_check_tor_with_service_as_a_string
-    disable_socks
-
-    is_tor_direct, ip_direct = check_tor_with_service_as_string
-    assert_equal(false, is_tor_direct)
-
-    enable_socks
-
-    is_tor_socks, ip_socks = check_tor_with_service_as_string
-    assert_equal(true, is_tor_socks)
-
-    assert(ip_direct != ip_socks)
-  end
-
-  def test_check_tor_via_net_http
-    disable_socks
-
-    tor_direct, ip_direct = check_tor
-    assert_equal(false, tor_direct)
-
-    tor_socks, ip_socks = check_tor(http_tor_proxy)
-    assert_equal(true, tor_socks)
-
-    assert(ip_direct != ip_socks)
-  end
-
-  def test_connect_to_ip
-    disable_socks
-
-    ip_direct = internet_yandex_com_ip
-
-    enable_socks
-
-    ip_socks = internet_yandex_com_ip
-
-    assert(ip_direct != ip_socks)
-  end
-
-  def test_connect_to_ip_via_net_http
-    disable_socks
-
-    ip_direct = internet_yandex_com_ip
-    ip_socks = internet_yandex_com_ip(http_tor_proxy)
-
-    assert(ip_direct != ip_socks)
-  end
-
-  def test_ignores
-    disable_socks
-
-    tor_direct, ip_direct = check_tor
-    assert_equal(false, tor_direct)
-
-    enable_socks
-    TCPSocket.socks_ignores << 'check.torproject.org'
-
-    tor_socks_ignored, ip_socks_ignored = check_tor
-    assert_equal(false, tor_socks_ignored)
-
-    assert(ip_direct == ip_socks_ignored)
-  end
-
-  def _get_http(http_klass, scheme, host, port, path, host_header)
-    body = nil
-    http_klass.start(host, port,
-                     :use_ssl => scheme == 'https',
-                     :verify_mode => OpenSSL::SSL::VERIFY_NONE) do |http|
-      req = Net::HTTP::Get.new path
-      req['Host'] = host_header
-      req['User-Agent'] = "ruby-socksify test"
-      body = http.request(req).body
-    end
-    body
-  end
-
-  def get_http(http_klass, url, host_header)
-    uri = URI(url)
-    _get_http(http_klass, uri.scheme, uri.host, uri.port, uri.request_uri, host_header)
-  end
-
-  def check_tor(http_klass = Net::HTTP)
-    parse_check_response get_http(http_klass, 'https://check.torproject.org/', 'check.torproject.org')
-  end
-
-  def check_tor_with_service_as_string(http_klass = Net::HTTP)
-    parse_check_response _get_http(http_klass, 'https', 'check.torproject.org', 'https', '/', 'check.torproject.org')
-  end
-
-  def internet_yandex_com_ip(http_klass = Net::HTTP)
-    parse_internet_yandex_com_response get_http(http_klass, 'https://213.180.204.62/internet', 'yandex.com') # "http://yandex.com/internet"
-  end
-
-  def parse_check_response(body)
-    if body.include? 'This browser is configured to use Tor.'
-      is_tor = true
-    elsif body.include? 'You are not using Tor.'
-      is_tor = false
-    else
-      raise 'Bogus response'
-    end
-
-    if body =~ /Your IP address appears to be:\s*<strong>(\d+\.\d+\.\d+\.\d+)<\/strong>/
-      ip = $1
-    else
-      raise 'Bogus response, no IP'
-    end
-    [is_tor, ip]
-  end
-
-  def parse_internet_yandex_com_response(body)
-    if body =~ /<strong>IP-[^<]*<\/strong>: (\d+\.\d+\.\d+\.\d+)/
-      ip = $1
-    else
-      raise 'Bogus response, no IP'+"\n"+body.inspect
-    end
-    ip
-  end
-
-  def test_resolve
-    enable_socks
-
-    assert_equal("8.8.8.8", Socksify::resolve("google-public-dns-a.google.com"))
-
-    assert_raise SOCKSError::HostUnreachable do
-      Socksify::resolve("nonexistent.spaceboyz.net")
-    end
-  end
-
-  def test_resolve_reverse
-    enable_socks
-
-    assert_equal("google-public-dns-a.google.com", Socksify::resolve("8.8.8.8"))
-
-    assert_raise SOCKSError::HostUnreachable do
-      Socksify::resolve("0.0.0.0")
-    end
-  end
-
-  def test_proxy
-    enable_socks 
-
-    default_server = TCPSocket.socks_server
-    default_port = TCPSocket.socks_port
-
-    Socksify.proxy('localhost.example.com', 60001) {
-      assert_equal TCPSocket.socks_server, 'localhost.example.com'
-      assert_equal TCPSocket.socks_port, 60001
-    }
-
-    assert_equal TCPSocket.socks_server, default_server
-    assert_equal TCPSocket.socks_port, default_port
-  end
-
-  def test_proxy_failback
-    enable_socks 
-
-    default_server = TCPSocket.socks_server
-    default_port = TCPSocket.socks_port
-
-    assert_raise StandardError do
-      Socksify.proxy('localhost.example.com', 60001) {
-        raise StandardError.new('error')
-      }
-    end
-
-    assert_equal TCPSocket.socks_server, default_server
-    assert_equal TCPSocket.socks_port, default_port
-  end
-end
-
-
-
diff --git a/test/test_helper.rb b/test/test_helper.rb
new file mode 100644
index 0000000..771f920
--- /dev/null
+++ b/test/test_helper.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+require 'minitest/autorun'
+require 'net/http'
+require 'uri'
+
+$LOAD_PATH.unshift File.expand_path("#{__dir__}/../lib")
+
+require 'socksify'
+require 'socksify/http'
+
+Socksify.debug = true
+
+module HelperMethods
+  def disable_socks
+    TCPSocket.socks_server = nil
+    TCPSocket.socks_port = nil
+  end
+
+  def enable_socks
+    TCPSocket.socks_server = '127.0.0.1'
+    TCPSocket.socks_port = 9050
+  end
+
+  def http_tor_proxy
+    Net::HTTP.socks_proxy('127.0.0.1', 9050)
+  end
+
+  def get_http(http_klass, url, host_header = nil)
+    uri = URI(url)
+    body = nil
+    http_klass.start(uri.host, uri.port,
+                     use_ssl: uri.scheme == 'https',
+                     verify_mode: OpenSSL::SSL::VERIFY_NONE) do |http|
+      req = Net::HTTP::Get.new uri.path
+      req['Host'] = host_header
+      body = http.request(req).body
+    end
+    body
+  end
+end
+
+module TorProjectHelperMethods
+  def parse_check_tor_resp(body)
+    ip_regexp = %r{Your IP address appears to be:\s*<strong>(\d+\.\d+\.\d+\.\d+)</strong>}
+    raise 'Bogus response, no IP' unless body =~ ip_regexp
+
+    [tor?(body), Regexp.last_match(1)] # true/false, ip
+  end
+
+  def tor?(tor_project_dot_org_body)
+    if tor_project_dot_org_body.include? 'This browser is configured to use Tor.'
+      true
+    elsif tor_project_dot_org_body.include? 'You are not using Tor.'
+      false
+    else
+      raise 'Bogus response'
+    end
+  end
+
+  def check_tor(http_klass = Net::HTTP)
+    parse_check_tor_resp get_http(http_klass, 'https://check.torproject.org/', 'check.torproject.org')
+  end
+
+  def check_tor_with_service_as_string(http_klass = Net::HTTP)
+    parse_check_tor_resp get_http(http_klass, 'https://check.torproject.org/')
+  end
+end
+
+module YandexHelperMethods
+  def internet_yandex_com_ip(http_klass = Net::HTTP)
+    parse_internet_yandex_com_response get_http(http_klass, 'https://213.180.204.62/internet', 'yandex.com') # "http://yandex.com/internet"
+  end
+
+  def parse_internet_yandex_com_response(body)
+    raise "Bogus response, no IP\n#{body.inspect}" unless body =~ %r{<div>(\d+\.\d+\.\d+\.\d+)</div>}
+
+    Regexp.last_match(1) # ip
+  end
+end
diff --git a/test/test_socksify.rb b/test/test_socksify.rb
new file mode 100644
index 0000000..5e80a4e
--- /dev/null
+++ b/test/test_socksify.rb
@@ -0,0 +1,143 @@
+# frozen_string_literal: true
+
+require_relative 'test_helper'
+
+# test class
+class SocksifyTest < Minitest::Test
+  include HelperMethods
+  include TorProjectHelperMethods
+  include YandexHelperMethods
+
+  def self.test_order
+    :alpha # until state between tests is fixed
+  end
+
+  if RUBY_VERSION.to_f < 3.1 # test legacy methods TCPSocket.socks_server= and TCPSocket.socks_port=
+    def test_check_tor
+      disable_socks
+
+      is_tor_direct, ip_direct = check_tor
+      refute is_tor_direct
+
+      enable_socks
+
+      is_tor_socks, ip_socks = check_tor
+      assert is_tor_socks
+
+      refute_equal ip_direct, ip_socks
+    end
+
+    def test_check_tor_with_service_as_a_string
+      disable_socks
+
+      is_tor_direct, ip_direct = check_tor_with_service_as_string
+      refute is_tor_direct
+
+      enable_socks
+
+      is_tor_socks, ip_socks = check_tor_with_service_as_string
+      assert is_tor_socks
+
+      refute_equal ip_direct, ip_socks
+    end
+
+    def test_connect_to_ip
+      disable_socks
+
+      ip_direct = internet_yandex_com_ip
+
+      enable_socks
+
+      ip_socks = internet_yandex_com_ip
+
+      refute_equal ip_direct, ip_socks
+    end
+  end
+  # end legacy method tests
+
+  def test_check_tor_via_net_http
+    disable_socks
+
+    tor_direct, ip_direct = check_tor
+    refute tor_direct
+
+    tor_socks, ip_socks = check_tor(http_tor_proxy)
+    assert tor_socks
+
+    refute_equal ip_direct, ip_socks
+  end
+
+  def test_connect_to_ip_via_net_http
+    disable_socks
+
+    ip_direct = internet_yandex_com_ip
+    ip_socks = internet_yandex_com_ip(http_tor_proxy)
+
+    refute_equal ip_direct, ip_socks
+  end
+
+  def test_ignores
+    disable_socks
+
+    tor_direct, ip_direct = check_tor
+    refute tor_direct
+
+    enable_socks
+    TCPSocket.socks_ignores << 'check.torproject.org'
+
+    tor_socks_ignored, ip_socks_ignored = check_tor
+    refute tor_socks_ignored
+
+    assert_equal ip_direct, ip_socks_ignored
+  end
+
+  def test_resolve
+    enable_socks
+
+    assert_includes ['8.8.8.8', '8.8.4.4'], Socksify.resolve('dns.google.com')
+
+    assert_raises SOCKSError::HostUnreachable do
+      Socksify.resolve('nonexistent.spaceboyz.net')
+    end
+  end
+
+  def test_resolve_reverse
+    enable_socks
+
+    assert_equal('dns.google', Socksify.resolve('8.8.8.8'))
+
+    assert_raises SOCKSError::HostUnreachable do
+      Socksify.resolve('0.0.0.0')
+    end
+  end
+
+  def test_proxy
+    enable_socks
+
+    default_server = TCPSocket.socks_server
+    default_port = TCPSocket.socks_port
+
+    Socksify.proxy('localhost.example.com', 60_001) do
+      assert_equal 'localhost.example.com', TCPSocket.socks_server
+      assert_equal 60_001, TCPSocket.socks_port
+    end
+
+    assert_equal [TCPSocket.socks_server, TCPSocket.socks_port], [default_server, default_port]
+  end
+
+  def test_proxy_failback
+    enable_socks
+
+    default_server = TCPSocket.socks_server
+    default_port = TCPSocket.socks_port
+
+    assert_raises StandardError do
+      Socksify.proxy('localhost.example.com', 60_001) do
+        raise StandardError, 'error'
+      end
+    end
+
+    assert_equal TCPSocket.socks_server, default_server
+    assert_equal TCPSocket.socks_port, default_port
+  end
+end

Debdiff

[The following lists of changes regard files as different if they have different names, permissions or owners.]

Files in second set of .debs but not in first

-rw-r--r--  root/root   /usr/lib/ruby/vendor_ruby/socksify/ruby3net_http_connectable.rb
-rw-r--r--  root/root   /usr/lib/ruby/vendor_ruby/socksify/socksproxyable.rb
-rw-r--r--  root/root   /usr/lib/ruby/vendor_ruby/socksify/tcpsocket.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/specifications/socksify-1.7.2.gemspec

Files in first set of .debs but not in second

-rw-r--r--  root/root   /usr/share/rubygems-integration/all/specifications/socksify-1.7.1.gemspec

No differences were encountered in the control files

More details

Full run details