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 -> 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.