Using Tcl to tunnel connections over SSH - part 2
Written by Wojciech Kocjan   
This article is a follow-up to a recent text on SSH tunnelling.

Previously we have created code that runs SSH command (plink in this case) and uses cat command on the other side to do a poor man's pinging. We'll now focus on installing a service and reading configuration file.

The configuration will be a plain ini file. Each section will define a remote machine to connect to along with tunnels that should be used for it. All options that should be passed to the sshtunnel instance should simply be specified as name=value pairs.

All tunnels should be specified in the form of tunnelN=type:localip:localport:remoteip:remoteport. Type should either be local or remote.

For example to define a connection to two hosts just create sshtunnels.ini file in the same directory as your output sshtunnels.exe file will be:

Sample configuration file
[host1]
hostname=host1.yourcompany.com
username=wkocjan
keyfile=C:/mykey.ppk
tunnel1=local:127.0.0.1:5901:127.0.0.1:5900

[host2]
hostname=host2.yourcompany.com
username=wkocjan
keyfile=C:/mykey.ppk
tunnel1=local:127.0.0.1:5902:127.0.0.1:5900

This will cause connection to two hosts to be created. On your machine TCP port 5901 will redirect to port 5900 on host1 and 5902 will redirect to 5900 on host2.

Reloading the configuration will require a restart of the Windows service and cause all connections to be dropped.

The code will reside in the same directories as previous part of the article did. Let's create the sshtunnels.vfs/main.tcl file. It will allow both installing the binary as a service and will be the main part of the service itself.

Since Windows services can't just be executables and tclsvc is part of commercial ActiveState TclDevKit, we will register our service using srvany.exe binary from Microsoft that allows registering any Windows binary as a service.

Let's start with beginning of our code - it will initialize a couple of variables:

Initialization of packages and variables
package require starkit
starkit::startup
 
package require sshtunnels
package require registry
package require twapi
package require inifile
 
set appdir [file dirname [info nameofexecutable]]
 
cd $appdir
set appdir [pwd]
 
catch {wm withdraw .}
 
set servicename SSHTunnels
 
switch -- [string trimleft [lindex $argv 0] -/] {
 

The switch will handle all modes of operations. Let's start with installation:

Handling of installation
    install {
        foreach binary {plink.exe srvany.exe} {
            set sbinary [file join -- $starkit::topdir bin $binary]
            set dbinary [file join -- $appdir $binary]
            if {![file exists $dbinary]} {
                file copy -force -- $sbinary $dbinary
            }
        }
 
        if {[catch {
            twapi::create_service $servicename \
                [file nativename [file join $appdir srvany.exe]] \
                -interactive 1 -starttype demand_start \
                -displayname "SSH tunnelling service"
        } error]} {
            tk_messageBox -icon error -message "Error while registering service: $error"
            exit 1
        }
 
        set regkey "HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\$servicename"
 
        registry set $regkey Description \
            "Allows configuring one or more SSH tunnels that can be started as a service"
        registry set "$regkey\\Parameters" Application \
            [file nativename [info nameofexecutable]]
        registry set "$regkey\\Parameters" AppParameters service
 
        tk_messageBox -message "Service installed successfully"
 
        exit 0
    }
 

Installation will extract binaries into the same directory the main binary will be in. It will also set up a Windows service called SSHTunnels. It will also set up parameters for srvany binary that will be the wrapper around main binary. Uninstallation will do something opposite:

Handling of uninstallation
    uninstall {
        catch {
            twapi::stop_service $servicename
        }
        if {[catch {
            twapi::delete_service $servicename
        }]} {
            tk_messageBox -icon error -message \
                "Error while unregistering service: $error"
            exit 1
        }
 
        tk_messageBox -message "Service uninstalled successfully"
 
        exit 0
    }
 

And now for the main part. It will read an ini file called the same as the binary - for example if binary is located as C:\sshtunnels\sshtunnels.exe then the ini file will be C:\sshtunnels\sshtunnels.ini.

Working as actual service
    service {
        set configfile [file rootname [info nameofexecutable]].ini
 
        if {[catch {        
            set ini [ini::open $configfile r]
        }]} {
            tk_messageBox -icon error -message \
                "Unable to read configuration file:\n[file nativename $configfile]"
            exit 0
        }
 
        foreach section [ini::sections $ini] {
            set obj ::tun$section
            set cmd [list sshtunnels::sshtunnel $obj]
 
            set tunnels [list]
 
            foreach {n v} [ini::get $ini $section] {
                if  {[regexp "^tunnel\[0-9\]+\$" $n]} {
                    lappend tunnels [split $v :]
                }  else  {
                    lappend cmd -$n $v
                }
            }
 
            lappend cmd -tunnels $tunnels
 
            if {[catch {
                eval $cmd
                $obj connect
            }]} {
                tk_messageBox -icon warning -message "Unable to read section $section - skipping"
            }
        }
    }
 

This case is the main part of the application.

It first finds the configuration file to read. It then reads all sections, creates a connection for each section - all attributes not starting with tunnel are passed as configuration options to the object.

Attributes starting with tunnel and suffixed with a number are assumed to be a tunnel definition. Each definition is split by : character and added to list of tunnels.

The final part of the code is closing the switch braces and waiting forever.

Entering main loop
}
vwait forever
 

We now have a complete application that we can wrap and run. We now need to build it by running:

Building binary file
C> sdx wrap sshtunnels.exe -runtime tclkit.exe

Next, in order to install it as a service run:

Installation as a service
C> sshtunnels.exe install

After you have prepared your configuration file, start the service by running:

Starting SSHTunnels service
C> net start "SSHTunnels"

A downloadable source code can be fetched from here: sshtunnels.vfs.zip

And a complete built version: sshtunnels.exe

Sample configuration file: sshtunnels-sample.ini

An interesting followup could be writing a small configuration utility that will allow editing the ini file.