Feb 20

Netcat Clone in Three Languages – Part I (Ruby)

I thought I’d continue my series of writing the same application in multiple languages by trying to clone the wonderful network tool: netcat. For the first installation I’m going to try it in Ruby.


NOTE: apparently not everyone is having great success. I have tried this out under cygwin and redhat enterprise linux 5, but if it doesn’t work for you please leave a comment and I’ll see what I can do.

The Program


The final script can be found here


The program. If you don’t know about netcat you should. It is one of the most useful command line tools out there when working with any network enabled program. You can use it to dump network data to files, files to network, tunnel udp over TCP, and more. I’m not actually going to mimic all that. I’m just going to try to support the following:



net_tool -c -h hostame -p port # should connect to hostname:port,
# forward data on STDIN along that connection,
# and forward any incoming data to STDOUT
net_tool -l -p port # should listen on port until a connection is established,
# forward data on STDIN along that connection,
#and forward any incoming data to STDOUT

Ruby


So the first contestant is ruby an object-oriented scripting language built for elegance over efficiency. It isn’t strictly necessary, but I’m going to create NetTools class to wrap all the logic in:

class NetTools
    def run(args)
        ...
    end
end
tools = NetTools.new
tools.run(ARGV)

Parsing Command Line Args


The first challenge is parsing the command line arguments, determining when there is an error, and providing a reasonable help message for the user. For this ruby provides the OptionParser class.

#!/usr/bin/ruby
require 'optparse'
require 'ostruct'
class NetTools
    def run(args)
        parse_opts(args)
    end 
    def parse_opts(args)
        @options = OpenStruct.new
        opts = OptionParser.new do |opts|
            opts.banner = "Usage: net_tool.irb [options]"
            opts.on("-c", "--connect",
                "Connect to a remote host") do
                @options.connection_type = :connect
            end
            opts.on("-l", "--listen",
                "Listen for a remote host to connect to this host") do
                @options.connection_type = :listen
            end
            opts.on("-r", "--remote-host HOSTNAME", String,
                "Specify the host to connect to") do |hostname|
                @options.hostname = hostname
            end
            opts.on("-p", "--port PORT", Integer,
                "Specify the TCP port") do |port|
                @options.port = port;
            end
            opts.on_tail("-h", "--help", "Show this message") do
                puts opts
                exit
            end
        end
        begin
            opts.parse!(args)
        rescue OptionParser::ParseError => err
            puts err.message
            puts opts
            exit
        end
        if (@options.connection_type == nil)
            puts "no connection type specified"
            puts opts
            exit
        end
        if (@options.port == nil)
            puts "no port specified"
            puts opts
            exit
        end
        if(@options.connection_type == :connect && 
                @options.hostname == nil)
            puts "connection type connect requires a hostname"
            puts opts
            exit
        end
    end
end
tools = NetTools.new
tools.run(ARGV)

So that’s pretty cute. It does most of the leg work for you. The only frustration I’d have it that it’s documentation is pretty weak when you want to specify anything beyond the basic options. Also there are at least two other ways of doing it so I’m not actually sure this is the standard mechanism.

Starting up the Socket


So once you’ve parsed your command line arguments the next step is to set up the socket. This actually is pretty easy. I added a new method connect_socket:

class NetTools
    ...
    def connect_socket()
        if(@options.connection_type == :connect)
            @socket = TCPSocket.open(@options.hostname, 
                               @options.port)
        else
            server = TCPServer.new(@options.port)
            server.listen( 1)
            @socket = server.accept
        end
    end
    ...
end

Asynchronous IO


The second part is to do some asynchronous IO. Basically we have to simultaneously forward the socket in to STDOUT and STDIN to socket out:

class NetTools
    ...
    def forward_data()
        while(true)
            begin
                while( (data = @socket.recv_nonblock(100)) != "")
                    STDOUT.write(data);
                end
                #if no data is available rather than EAGAIN
                #that is EOF
                exit
            rescue Errno::EAGAIN
            end
            begin
                while( (data = STDIN.read_nonblock(100)) != "")
                    @socket.write(data);
                end
            rescue Errno::EAGAIN
            rescue EOFError
                #STDIN uses EOFError
                exit
            end
            IO.select([@socket, STDIN], [@socket, STDIN], 
                [@socket, STDIN])
        end
    end
    ....
end

Ruby seems to be a little uneven here in two things:

  1. Some conditions are exceptions and some are C-style return values. Here ruby is showing its ties to the underlying language I suppose but why is having nothing available an exception while EOF is a special case return value than depends on the type of socket object.
  2. It isn’t at all clear when navigating the documentation how the write method is related to the socket object or STDIN. It’s documented under IO and not referenced from any of the socket documentation

Conclusion

I seem to have this problem a lot with scripting languages, but I find that the basic script is pretty easy, but the details (like correctly handling stdin EOF, asyncronous IO, extra options in the command line option parser, etc) are not well documented enough to easily produce a robust application. I’ve spent about maybe 15 minutes of actual script writing which is good, but about 3 hours of digging through disparate documentation trying to find the details of what I need which is annoying. If I wanted to do that I’d just write the thing in C.

That being said this wasn’t too bad. I’ll run it past the local ruby guru to see what he has to say. I thought it was interesting that the most complicated part of the script was parsing the command line options though.

The Author

Michael Smit is a software engineer in Seattle, Washington who works for amazon

3 comments

Comments are closed.