Using Tcl to tunnel connections over SSH
Written by Wojciech Kocjan   
First of all I need to apologize that there have been no posts for such a long time. I am currently in the process of writing a book about Nagios and have little time to dedicate to the blog. I hope that'll improve really soon. Today's article talks about how to use Tcl to make a Microsoft Windows service that runs multiple SSH connections and tunnels ports. We'll also use only free tools so solution will be applicable to everyone.

This article is strictly Windows specific. It is also much larger than usual. I decided to create a more complex, but complete solution than to show some parts and just hope everyone believes it works. I hope that this tool will actually be useful for a couple of people and that might attract them to use Tcl.

The text will also be split in two parts - first one will cover wrapper around SSH client itself. Second one will discuss wrapping it all up in a single binary that will automatically create Windows services. It will also cover how to configure the tool.

The reason this is done as a service is that you can actually have other services that should depend on this one being running. If you can't wait to see how it works just jump to the bottom to download it.

Ok, here goes. Let's start with creating file structure. Let's create the following:
Now we can start coding Tcl. Let's start with is a Itcl class that will handle a single connection along with tunnelling it. We'll also implement pinging the other side (using cat system command). Let's create it in sshtunnels.vfs/lib/sshtunnels/sshtunnels.tcl file. Definition of the class is as follows:

Itcl class for managing a single SSH connection
package provide sshtunnels::sshtunnel 1.0
 
namespace eval sshtunnels {}
 
package require Itcl
 
itcl::class sshtunnels::sshtunnel {
    # ssh parameters
    public variable hostname localhost
    public variable username ""
    public variable port ""
    public variable password ""
    public variable keyfile ""
    public variable sshcommand {plink -ssh}
    public variable tunnels {}
 
    # timer events for connecting and pinging
    private variable timerConnect ""
    private variable timerPing ""
 
    # pipe to ssh command
    private variable pipe ""
    private variable pingcount 0
 
    constructor {args} {
        eval configure $args
    }
 
    destructor {
        after cancel $timerConnect
        after cancel $timerPing
        catch {close $pipe}
    }
 
    # mechanisms for logging information - mostly for debugging
    private method dolog {mode string}
 
    # connect to SSH server
    public method connect {}
 
    # disconnect from SSH server
    public method disconnect {}
 
    # connect later - if connecting right now fails
    private method _connectLater {}
 
    public method doping {}
 
    # handle pong 
    private method dopong {}
 
    # ping each 110 seconds
    private method _pingLater {}
}
 

The class has a couple of options it accepts and mainly methods to connect/disconnect and handle ping/pong operations. Destructor tries to clean up events and close pipe handle. Constructor passes all arguments to configure method.

For now we won't use any logging, adding a simple log file handling is left as an exercise. So let's define an empty body for it:

Logging stub that
itcl::body sshtunnels::sshtunnel::dolog {mode string} { }

And now comes the harder part - we need to define methods for connecting and disconnecting from server.

Connecting and disconnecting methods
itcl::body sshtunnels::sshtunnel::connect {} {
    after cancel $timerConnect
    after cancel $timerPing
 
    set command $sshcommand
 
    if {$username != ""} {
        lappend command "-l" $username
    }
    if {$port != ""} {
        lappend command "-P" $port
    }
    if {$password != ""} {
        lappend command "-pw" $password
    }
    if {$keyfile != ""} {
        lappend command "-i" $keyfile
    }
 
    lappend command $hostname
 
    foreach tunnel $tunnels {
        foreach {type localip localport remoteip remoteport} $tunnel break
        switch -- $type {
            local {
                if {$localip == ""} {
                    lappend command "-L" "${localport}:${remoteip}:${remoteport}"
                }  else  {
                    lappend command "-L" "${localip}:${localport}:${remoteip}:${remoteport}"
                }
            }
            remote {
                if {$localip == ""} {
                    lappend command "-R" "${localport}:${remoteip}:${remoteport}"
                }  else  {
                    lappend command "-R" "${localip}:${localport}:${remoteip}:${remoteport}"
                }
            }
        }
    }
 
    lappend command "cat"
 
    dolog debug "Command to run: $command"
 
    catch {close $pipe}
 
    if {[catch {
        set pipe [open "|$command" r+]
        fconfigure $pipe -translation binary -blocking 0 -buffering line
        fileevent $pipe readable [itcl::code $this dopong]
        after 500
        puts $pipe "y"
        flush $pipe
    } error]} {
        set pipe ""
        dolog error $error
        _connectLater
    }  else  {
        set pingcount 0
        _pingLater
        after cancel $timerConnect
    }
}
 
itcl::body sshtunnels::sshtunnel::disconnect {} {
    after cancel $timerPing
    after cancel $timerConnect
    catch {close $pipe}
    set pipe ""
}
 
itcl::body sshtunnels::sshtunnel::_connectLater {} {
    after cancel $timerConnect
    set timerConnect [after 10000 [itcl::code $this connect]]
}
 

The first method builds a command, opens a pipe and if that fails it will try to reopen it after a while. All parameters are passed to plink. After creating the pipe to plink, we send an "y" text that will cause question whether to accept key to pass. Every line that is received from plink is passed to dopong method which will handle results from pings.

Just so it makes things a bit more clear, below is help for plink command:

Plink usage
Usage: plink [options] [user@]host [command]
       ("host" can also be a PuTTY saved session name)
Options:
  -V        print version information and exit
  -pgpfp    print PGP key fingerprints and exit
  -v        show verbose messages
  -load sessname  Load settings from saved session
  -ssh -telnet -rlogin -raw
            force use of a particular protocol
  -P port   connect to specified port
  -l user   connect with specified username
  -batch    disable all interactive prompts
The following options only apply to SSH connections:
  -pw passw login with specified password
  -D [listen-IP:]listen-port
            Dynamic SOCKS-based port forwarding
  -L [listen-IP:]listen-port:host:port
            Forward local port to remote address
  -R [listen-IP:]listen-port:host:port
            Forward remote port to local address
  -X -x     enable / disable X11 forwarding
  -A -a     enable / disable agent forwarding
  -t -T     enable / disable pty allocation
  -1 -2     force use of particular protocol version
  -4 -6     force use of IPv4 or IPv6
  -C        enable compression
  -i key    private key file for authentication
  -m file   read remote command(s) from file
  -s        remote command is an SSH subsystem (SSH-2 only)
  -N        don't start a shell/command (SSH-2 only)

Next, on disconnect we close the pipe and set the filehandle to an empty string to avoid closing a different file handle by accident.

Finally, in order to detect when a link has become broken, we need to implement a simple ping that will periodically send a message which should be sent back by the cat command we run. If it has not returned after several ones have been sent, we assume the connection is broken.

Handling ping and ping messages
itcl::body sshtunnels::sshtunnel::doping {} {
    if {[catch {
        puts $pipe "DOPING"
        flush $pipe
    }]} {
        dolog error "Ping failed"
        after cancel $timerPing
        after cancel $timerConnect
        catch {close $pipe}
        set pipe ""
        _connectLater
    }  else  {
        incr pingcount
        dolog debug "Ping succeded - ping count is $pingcount"
    }
}
 
itcl::body sshtunnels::sshtunnel::dopong {} {
    catch {set eof [eof $pipe]}
    if {!$eof} {
        set eof [catch {gets $pipe line}]
    }
    if {!$eof} {
        dolog debug "Received line: $line"
    }  else  {
        dolog error "Received EOF - reconnecting in 10 seconds"
    }
 
    if {$eof} {
        dolog error "Pong failed"
        after cancel $timerPing
        catch {close $pipe}
        set pipe ""
        _connectLater
    }  elseif {$line == "DOPING"} {
        incr pingcount -1
        if {$pingcount < 0} {
            set pingcount 0
        }
    }  else  {
        dolog error "Received unknown line text: $line"
    }
}
 
itcl::body sshtunnels::sshtunnel::_pingLater {} {
    after cancel $timerPing
    set timerPing [after 110000 [itcl::code $this doping]]
}
 

Now we'll need to add sshtunnels.vfs/lib/sshtunnels/pkgIndex.tcl file, either automatically using pkg_mkIndex Tcl command or manually with the specified contents:

Package definition
package ifneeded sshtunnels 1.0 [list source [file join $dir sshtunnels.tcl]]
 

Next article will cover main part of the application that will read configuration and handle running as a system service.