Tag Archives: tcp

A TCP proxy in Ruby

A TCP proxy (or a tunnel, or a bridge) is a program that listens at a certain network address for connections. Whenever a connection is made to that address, the program connects to another predefined network address and starts transferring data between the two ends.

The reason I wanted a TCP proxy is this: I needed to run a program on a virtual machine. This program needs Internet access, but I couldn’t make the VM’s Internet access to work — it could only connect to programs on the host machine, i.e. my computer. I thus used a TCP proxy on the host machine to connect the VM to the outside world. (The fact that the program running on the VM needed to access only one predefined network address simplified things greatly).

Below is a Ruby script I used, made from bits of example code that I found on the Web. I tested it with Ruby 1.8.6 on Windows 7.

Several notes regarding the script:

Preventing threads from disappearing

The script is designed to exit with a stack trace on exception. More extensive error handling would be overkill for a quick script. The problem is that in Ruby, by default, threads silently exit on exception — it caused me quite a headache before figuring this out. This is fixed by setting Thread.abort_on_exception to true.

Exiting with Ctrl-C

It’s nice to be able to exit the script by pressing Ctrl-C. On Windows, Ruby doesn’t handle Ctrl-C keypresses inside socket.accept (and apparently during other blocking calls). To fix this, we need a special thread that spends most of its life sleeping, but wakes up once in a second. During that time Ruby will be able to process the keypress and exit.

The script

require 'socket'

if ARGV.length < 1
    $stderr.puts "Usage: #{$0} remoteHost:remotePort [ localPort [ localHost ] ]"
    exit 1
end

$remoteHost, $remotePort = ARGV.shift.split(":")
puts "target address: #{$remoteHost}:#{$remotePort}"
localPort = ARGV.shift || $remotePort
localHost = ARGV.shift

$blockSize = 1024

server = TCPServer.open(localHost, localPort)

port = server.addr[1]
addrs = server.addr[2..-1].uniq

puts "*** listening on #{addrs.collect{|a|"#{a}:#{port}"}.join(' ')}"

# abort on exceptions, otherwise threads will be silently killed in case
# of unhandled exceptions
Thread.abort_on_exception = true

# have a thread just to process Ctrl-C events on Windows
# (although Ctrl-Break always works)
Thread.new { loop { sleep 1 } }

def connThread(local)
    port, name = local.peeraddr[1..2]
    puts "*** receiving from #{name}:#{port}"

    # open connection to remote server
    remote = TCPSocket.new($remoteHost, $remotePort)
    
    # start reading from both ends
    loop do
        ready = select([local, remote], nil, nil)
        if ready[0].include? local
            # local -> remote
            data = local.recv($blockSize)
            if data.empty?
                puts "local end closed connection"
                break
            end
            remote.write(data)
        end
        if ready[0].include? remote
            # remote -&gt; local
            data = remote.recv($blockSize)
            if data.empty?
                puts "remote end closed connection"
                break
            end
            local.write(data)
        end
    end
    
    local.close
    remote.close
    
    puts "*** done with #{name}:#{port}"
end

loop do
    # whenever server.accept returns a new connection, start
    # a handler thread for that connection
    Thread.start(server.accept) { |local| connThread(local) }
end

PS

When I started writing this script I got a cryptic error message if a didn’t add a “require ‘rubygems'” line at the beginning. However I can’t reproduce the problem now. In fact the browser history doesn’t show all the googling I’ve done to find the solution and I’m beginning to think that I hallucinated it all.

Advertisements