1#!/bin/sh 2# 3# ftpgw.tcl -- FTP gateway to permit tunnelling 4# 5# Usage: ftpgw.tcl [-p port-range] [-v] [listen-port [ftpd-host [ftpd-port]]] 6# 7# This program is a simple FTP gateway that intercepts PORT commands and 8# passive-mode responses so that it can set up handlers for the data 9# streams associated with them. It then rewites these control lines so 10# that traffic to an FTP server apparently comes from the gateway process. 11# Similarly a client sees a remote port that it can access for passive 12# mode transfers. This allows the FTP control (but not data) channel to 13# be tunnelled and encrypted. 14# 15# By default the program listens on port 2121 and will redirect traffic 16# to a local FTP server on port 21. These values can be overridden on the 17# command line. The -v option turns on verbose logging to stderr. 18# 19# If the -p option is specified then the argument is a range of port 20# numbers in the form xxx-yyy. All passive-mode data ports will be in 21# the range xxx to yyy and the response lines will be re-written to 22# redirect a client to the corresponding port on 127.0.0.1 (localhost). 23# This means that passive-mode data connections can be tunnelled in 24# addition to the control connection. 25# 26# 27# This file is part of "zebedee". 28# 29# Copyright 2000, 2001 by Neil Winton. All rights reserved. 30# 31# This program is free software; you can redistribute it and/or modify 32# it under the terms of the GNU General Public License as published by 33# the Free Software Foundation; either version 2 of the License, or 34# (at your option) any later version. 35# 36# This program is distributed in the hope that it will be useful, 37# but WITHOUT ANY WARRANTY; without even the implied warranty of 38# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 39# GNU General Public License for more details. 40# 41# You should have received a copy of the GNU General Public License 42# along with this program; if not, write to the Free Software 43# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 44# 45# For further details on "zebedee" see http://www.winton.org.uk/zebedee/ 46# 47# 48# $Id: ftpgw.tcl,v 1.2 2001/04/13 17:42:30 ndwinton Exp $ 49# 50# Restart using tclsh. Do not delete this backslash -> \ 51 exec tclsh $0 ${1+"$@"} 52 53set ListenPort 2121 ;# Port on which to listen 54set FtpdHost localhost ;# Host on which ftpd is running 55set FtpdPort 21 ;# Port on which ftpd is listening 56set FtpdAddr 127.0.0.1 ;# Address of host on which ftpd is running 57set Verbose 0 ;# Verbose mode -- log messages to stderr 58set Initialised 0 ;# Flag to indicate initialisation complete 59set MinPasvPort 0 ;# Minimum value for passive data port 60set MaxPasvPort 0 ;# Maximum value for passive data port 61 62 63# log 64# 65# Log a message in verbose mode 66 67proc log {msg} { 68 global Verbose 69 70 if {$Verbose} {puts stderr $msg} 71} 72 73# acceptCtrlConn 74# 75# Accept a new control connection and create a socket to the real ftpd. 76# Traffic on either connection is handled by the handleCtrl routine. 77 78proc acceptCtrlConn {mySock ipAddr port} { 79 global FtpdHost FtpdPort Initialised 80 81 if {!$Initialised} { 82 # First connection received will be a dummy to determine the host 83 # address -- ignore it 84 set Initialised 1 85 close $mySock 86 return 87 } 88 89 log "$mySock: new client from $ipAddr/$port" 90 91 if {[catch {socket $FtpdHost $FtpdPort} ftpdSock]} { 92 close $mySock 93 error "can't create forwarding control connection to $FtpdHost/$FtpdPort: $ftpdSock" 94 } 95 96 log "$mySock: connected to $FtpdHost/$FtpdPort via $ftpdSock" 97 98 fconfigure $mySock -blocking false 99 fconfigure $ftpdSock -blocking false 100 101 fileevent $mySock readable [list handleCtrl $mySock $ftpdSock] 102 fileevent $ftpdSock readable [list handleCtrl $ftpdSock $mySock] 103} 104 105# handleCtrl 106# 107# Handle a control connection. This is used for both traffic from and to 108# the server. Data is read from fromSock and written to toSock. It may 109# be transformed before being written. Specifically, PORT commands from 110# a client result and passive-mode replies (227) from a server result in 111# a new local data socket and handler being created and the address details 112# being rewritten. 113 114proc handleCtrl {fromSock toSock} { 115 global HostAddr FtpdHost FtpdAddr MaxPasvPort MinPasvPort DataSock 116 117 # Check for EOF and close connections if necessary 118 119 if {[gets $fromSock line] < 0} { 120 close $fromSock 121 close $toSock 122 } { 123 # Make sure we do not show passwords in verbose output 124 125 if {[string match "PASS *" $line]} { 126 log "$fromSock -> $toSock: PASS <password>" 127 } { 128 log "$fromSock -> $toSock: $line" 129 } 130 131 # Re-write PORT command lines from the client. 132 133 if {[regexp -nocase {^PORT ([0-9]+),([0-9]+),([0-9]+),([0-9]+),([0-9]+),([0-9]+)} $line dummy a1 a2 a3 a4 p1 p2]} { 134 log "$fromSock -> $toSock: Rewriting $line" 135 136 set clientAddr "$a1.$a2.$a3.$a4" 137 set clientPort [expr {$p1 * 256 + $p2}] 138 set handler [list acceptDataConn [list $FtpdAddr 127.0.0.1] $clientAddr $clientPort] 139 140 if {[catch {createDataConn $handler 1024 65535} connInfo]} { 141 close $fromSock 142 close $toSock 143 log "$fromSock -> $toSock: Error creating data connection: $connInfo" 144 return 145 } 146 147 # Note the socket handle used for this address/port combination 148 # so that we can close it after a connection has been accepted. 149 150 set DataSock($clientAddr,$clientPort) [lindex $connInfo 0] 151 152 # Construct new PORT command referring to the new local 153 # data socket. 154 155 set port [lindex $connInfo 1] 156 set port [expr {$port / 256}],[expr {$port % 256}] 157 158 # If the ftpd is running locally then we need to supply 159 # the localhost address otherwise the full machine IP 160 # address is needed for the data connection to appear to 161 # come from the same place as the control connection. 162 163 if {$FtpdAddr == "127.0.0.1"} { 164 set myAddr "127.0.0.1" 165 } { 166 set myAddr $HostAddr 167 } 168 set myAddr [join [split $myAddr .] ,] 169 170 set line "PORT $myAddr,$port" 171 172 log "$fromSock -> $toSock: Rewritten to $line" 173 } 174 175 # Rewrite passive mode lines response lines from server 176 177 if {[regexp {^227 .*[^0-9]([0-9]+),([0-9]+),([0-9]+),([0-9]+),([0-9]+),([0-9]+)} $line dummy a1 a2 a3 a4 p1 p2]} { 178 log "$fromSock -> $toSock: Rewriting $line" 179 180 set serverAddr "$a1.$a2.$a3.$a4" 181 set serverPort [expr {$p1 * 256 + $p2}] 182 183 if {$MinPasvPort} { 184 set allowed 127.0.0.1 185 } { 186 set allowed {} 187 } 188 set handler [list acceptDataConn $allowed $serverAddr $serverPort] 189 190 if {[catch {createDataConn $handler $MinPasvPort $MaxPasvPort} connInfo]} { 191 close $fromSock 192 close $toSock 193 log "$fromSock -> $toSock: Error creating data connection: $connInfo" 194 return 195 } 196 197 # Note the socket handle used for this address/port combination 198 # so that we can close it after a connection has been accepted. 199 200 set DataSock($serverAddr,$serverPort) [lindex $connInfo 0] 201 202 # Construct a 227 response line referring to the new local 203 # data socket. 204 205 set port [lindex $connInfo 1] 206 set port [expr {$port / 256}],[expr {$port % 256}] 207 208 # If a port range has been specified then we must only supply 209 # the localhost address because this is being used for 210 # tunnelling and for this to work the client must connect to 211 # its matching local port. 212 213 if {$MinPasvPort} { 214 set myAddr "127.0.0.1" 215 } { 216 set myAddr $HostAddr 217 } 218 set myAddr [join [split $myAddr .] ,] 219 set line "227 Entering Passive Mode ($myAddr,$port)" 220 221 log "$fromSock -> $toSock: Rewritten to $line" 222 } 223 224 puts $toSock $line 225 flush $toSock 226 } 227} 228 229# createDataConn 230# 231# Create a new data connection listener socket with handler command "cmd". 232# The function returns the port number. The "loPort" and "hiPort" parameters 233# give the range in which the port should lie. If loPort is zero then any 234# port (>= 1024) is acceptable. Within the range we try to pick a port at 235# random (sort of :-) to avoid the worst excesses of passive port stealing. 236# 237# Returns a list of the socket handle and port number 238 239proc createDataConn {cmd loPort hiPort} { 240 241 if {$loPort < 1024} { 242 set loPort 1024 243 } 244 if {$hiPort > 65535 || $hiPort < 1024} { 245 set hiPort 65535 246 } 247 248 set count [expr {$hiPort - $loPort + 1}] 249 250 # Pick a random starting point 251 252 set start [expr {int(rand() * $count)}] 253 254 for {set i 0} {$i < $count} {incr i} { 255 256 set port [expr {(($i + $start) % $count) + $loPort}] 257 258 if {![catch {socket -server $cmd $port} sock]} { 259 break 260 } 261 } 262 263 if {$i >= $count} { 264 error "can't find free data port socket" 265 } 266 267 return [list $sock $port] 268} 269 270# acceptDataConn 271# 272# Accept a new data connection and set up forwarding data channel handlers 273# (in binary data mode) to the address and port in toAddr/toPort. The 274# connection will be rejected unless it comes from an address named in 275# allowFrom, if set, to avoid port "theft". 276 277proc acceptDataConn {allowFrom toAddr toPort mySock ipAddr port} { 278 global DataSock 279 280 log "$mySock: new data connection from $ipAddr/$port" 281 282 if {$allowFrom != {} && [lsearch -exact $allowFrom $ipAddr] == -1} { 283 log "$mySock: WARNING: rejected connection from $ipAddr" 284 close $mySock 285 return 286 } 287 288 # Once a data connection has been accepted we can close the listening 289 # socket. It will only be used once. 290 291 if {[info exists DataSock($toAddr,$toPort)]} { 292 close $DataSock($toAddr,$toPort) 293 unset DataSock($toAddr,$toPort) 294 } 295 296 # Open a connection to the real destination 297 298 if {[catch {socket $toAddr $toPort} toSock]} { 299 close $mySock 300 error "can't make forwarding data connection to $toAddr/$toPort: $toSock" 301 } 302 303 log "$mySock: forwards to $toSock" 304 305 # Make the channels handle binary data 306 307 fconfigure $toSock -translation binary 308 fconfigure $mySock -translation binary 309 310 # Set up data transfer based on which end of the pipe becomes readable 311 312 fileevent $mySock readable [list startCopy $mySock $toSock] 313 fileevent $toSock readable [list startCopy $toSock $mySock] 314} 315 316# startCopy 317# 318# Set up fcopy to handle copying the data in the background from fromSock to 319# toSock. 320 321proc startCopy {fromSock toSock} { 322 323 fileevent $toSock readable {} 324 fileevent $fromSock readable {} 325 326 log "$fromSock -> $toSock: starting data copy" 327 fcopy $fromSock $toSock -command [list finishCopy $fromSock $toSock] 328} 329 330# finishCopy 331# 332# Handler routine for end of fcopy 333 334proc finishCopy {fromSock toSock bytes {error {}}} { 335 if {"$error" != {}} { 336 log "$fromSock -> $toSock: error copying data: $error" 337 } 338 339 log "$fromSock -> $toSock: copy finished, $bytes bytes transferred" 340 341 catch { 342 close $fromSock 343 close $toSock 344 } 345} 346 347# bgerror 348# 349# Handle an error -- just print a message in verbose mode 350 351proc bgerror {args} { 352 log "ERROR: $args" 353} 354 355### 356### Main Code 357### 358 359for {set i 0} {$i < [llength $argv]} {incr i} { 360 switch -exact -- [lindex $argv $i] { 361 {-v} { 362 incr Verbose 363 } 364 365 {-p} { 366 incr i 367 if {[scan [lindex $argv $i] "%hu-%hu" MinPasvPort MaxPasvPort] != 2} { 368 error "$argv0: invalid range to -r: [lindex $argv $i]" 369 } 370 if {$MinPasvPort < 1024} { 371 error "$argv0: minimum passive data port must be >= 1024" 372 } 373 if {$MinPasvPort > $MaxPasvPort} { 374 error "$argv0: minimum passive data port must be <= maximum" 375 } 376 } 377 378 default { 379 break 380 } 381 } 382} 383 384set argv [lrange $argv $i end] 385 386if {[lindex $argv 0] != {}} { 387 set ListenPort [lindex $argv 0] 388} 389if {[lindex $argv 1] != {}} { 390 set FtpdHost [lindex $argv 1] 391} 392if {[lindex $argv 2] != {}} { 393 set FtpdPort [lindex $argv 2] 394} 395 396# Try connecting to the ftpd server both to validate the date and to 397# get its IP address 398 399log "Contacting FTP server on $FtpdHost/$FtpdPort" 400 401if {[catch {socket $FtpdHost $FtpdPort} s]} { 402 error "$argv0: can't contact FTP server on $FtpdHost/$FtdPort" 403} 404set FtpdAddr [lindex [fconfigure $s -peername] 0] 405close $s 406 407# Start the local listener 408 409set Listener [socket -server acceptCtrlConn $ListenPort] 410 411log "Listening on port $ListenPort" 412 413# Get the local IP address. We do this by making a connection to the 414# port we have just set up for listening and then using fconfigure. Note 415# that on Windows systems fconfigure can take an unexpectedly long time. 416 417log "Determining the host address ..." 418 419set s [socket [info hostname] $ListenPort] 420set HostAddr [lindex [fconfigure $s -sockname] 0] 421close $s 422 423log "Host [info hostname], address $HostAddr" 424 425# Enter the Tcl event loop ... 426 427vwait forever 428