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