From e5ffd58bf7202535790ac989f29d1afc7cb92972 Mon Sep 17 00:00:00 2001 From: Pierre Massat Date: Thu, 30 Jun 2016 18:20:35 -0700 Subject: [PATCH 1/2] Allow server to be behind a NAT --- cmd.go | 18 +++++------------- conn.go | 26 +++++++++++++++++++++++--- server.go | 10 ++++++++++ socket.go | 19 ++++++++++++++----- 4 files changed, 52 insertions(+), 21 deletions(-) diff --git a/cmd.go b/cmd.go index cc4524e..73b7f3e 100644 --- a/cmd.go +++ b/cmd.go @@ -310,14 +310,14 @@ func (cmd commandEpsv) RequireAuth() bool { } func (cmd commandEpsv) Execute(conn *Conn, param string) { - addr := conn.conn.LocalAddr() - lastIdx := strings.LastIndex(addr.String(), ":") + addr := conn.PublicIp() + lastIdx := strings.LastIndex(addr, ":") if lastIdx <= 0 { conn.writeMessage(425, "Data connection failed") return } - socket, err := newPassiveSocket(addr.String()[:lastIdx], conn.logger, conn.tlsConfig) + socket, err := newPassiveSocket(addr[:lastIdx], conn.PassivePort(), conn.logger, conn.tlsConfig) if err != nil { log.Error(err) conn.writeMessage(425, "Data connection failed") @@ -592,13 +592,7 @@ func (cmd commandPasv) RequireAuth() bool { } func (cmd commandPasv) Execute(conn *Conn, param string) { - addr := conn.conn.LocalAddr() - parts := strings.Split(addr.String(), ":") - if len(parts) != 2 { - conn.writeMessage(425, "Data connection failed") - return - } - socket, err := newPassiveSocket(parts[0], conn.logger, conn.tlsConfig) + socket, err := newPassiveSocket(conn.PublicIp(), conn.PassivePort(), conn.logger, conn.tlsConfig) if err != nil { conn.writeMessage(425, "Data connection failed") return @@ -606,9 +600,7 @@ func (cmd commandPasv) Execute(conn *Conn, param string) { conn.dataConn = socket p1 := socket.Port() / 256 p2 := socket.Port() - (p1 * 256) - host := socket.Host() - - quads := strings.Split(host, ".") + quads := strings.Split(conn.PublicIp(), ".") target := fmt.Sprintf("(%s,%s,%s,%s,%d,%d)", quads[0], quads[1], quads[2], quads[3], p1, p2) msg := "Entering Passive Mode " + target conn.writeMessage(227, msg) diff --git a/conn.go b/conn.go index e6a8c08..7e303d4 100644 --- a/conn.go +++ b/conn.go @@ -8,10 +8,13 @@ import ( "encoding/hex" "fmt" "io" + "log" "net" "path/filepath" "strconv" "strings" + + mrand "math/rand" ) const ( @@ -47,6 +50,24 @@ func (conn *Conn) IsLogin() bool { return len(conn.user) > 0 } +func (conn *Conn) PublicIp() string { + return conn.server.PublicIp +} + +func (conn *Conn) PassivePort() int { + portRange := strings.Split(conn.server.PassivePorts, "-") + + if len(portRange) != 2 { + log.Println("empty port") + return 0 + } + + minPort, _ := strconv.Atoi(strings.TrimSpace(portRange[0])) + maxPort, _ := strconv.Atoi(strings.TrimSpace(portRange[1])) + + return minPort + mrand.Intn(maxPort-minPort) +} + // returns a random 20 char string that can be used as a unique session ID func newSessionID() string { hash := sha256.New() @@ -72,11 +93,10 @@ func (conn *Conn) Serve() { for { line, err := conn.controlReader.ReadString('\n') if err != nil { - if err == io.EOF { - continue + if err != io.EOF { + conn.logger.Print(fmt.Sprintln("read error:", err)) } - conn.logger.Print(fmt.Sprintln("read error:", err)) break } conn.receiveLine(line) diff --git a/server.go b/server.go index ba25294..559d256 100644 --- a/server.go +++ b/server.go @@ -27,6 +27,12 @@ type ServerOpts struct { // "::", which means all hostnames on ipv4 and ipv6. Hostname string + // Public IP of the server + PublicIp string + + // Passive ports + PassivePorts string + // The port that the FTP should listen on. Optional, defaults to 3000. In // a production environment you will probably want to change this to 21. Port int @@ -58,6 +64,7 @@ type Server struct { logger *Logger listener net.Listener tlsConfig *tls.Config + publicIp string } // serverOptsWithDefaults copies an ServerOpts struct into a new struct, @@ -99,6 +106,9 @@ func serverOptsWithDefaults(opts *ServerOpts) *ServerOpts { newOpts.CertFile = opts.CertFile newOpts.ExplicitFTPS = opts.ExplicitFTPS + newOpts.PublicIp = opts.PublicIp + newOpts.PassivePorts = opts.PassivePorts + return &newOpts } diff --git a/socket.go b/socket.go index f0e22cb..231e313 100644 --- a/socket.go +++ b/socket.go @@ -3,6 +3,7 @@ package server import ( "crypto/tls" "errors" + "fmt" "net" "strconv" "strings" @@ -33,24 +34,31 @@ type ftpActiveSocket struct { logger *Logger } -func newActiveSocket(host string, port int, logger *Logger) (DataSocket, error) { - connectTo := buildTCPString(host, port) +func newActiveSocket(remote string, port int, logger *Logger) (DataSocket, error) { + connectTo := buildTCPString(remote, port) + logger.Print("Opening active data connection to " + connectTo) + raddr, err := net.ResolveTCPAddr("tcp", connectTo) + if err != nil { logger.Print(err) return nil, err } + tcpConn, err := net.DialTCP("tcp", nil, raddr) + if err != nil { logger.Print(err) return nil, err } + socket := new(ftpActiveSocket) socket.conn = tcpConn - socket.host = host + socket.host = remote socket.port = port socket.logger = logger + return socket, nil } @@ -85,12 +93,13 @@ type ftpPassiveSocket struct { tlsConfing *tls.Config } -func newPassiveSocket(host string, logger *Logger, tlsConfing *tls.Config) (DataSocket, error) { +func newPassiveSocket(host string, port int, logger *Logger, tlsConfing *tls.Config) (DataSocket, error) { socket := new(ftpPassiveSocket) socket.ingress = make(chan []byte) socket.egress = make(chan []byte) socket.logger = logger socket.host = host + socket.port = port if err := socket.GoListenAndServe(); err != nil { return nil, err } @@ -128,7 +137,7 @@ func (socket *ftpPassiveSocket) Close() error { } func (socket *ftpPassiveSocket) GoListenAndServe() (err error) { - laddr, err := net.ResolveTCPAddr("tcp", "0.0.0.0:0") + laddr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("0.0.0.0:%d", socket.port)) if err != nil { socket.logger.Print(err) return From fcd8a9cd617f09cf136b1397ed7e76297ea8201a Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sun, 10 Jul 2016 11:13:38 +0800 Subject: [PATCH 2/2] Compitable with no special public ip and ports range --- cmd.go | 9 ++++++--- conn.go | 27 +++++++++++++++++++-------- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/cmd.go b/cmd.go index 73b7f3e..d725d3a 100644 --- a/cmd.go +++ b/cmd.go @@ -310,7 +310,7 @@ func (cmd commandEpsv) RequireAuth() bool { } func (cmd commandEpsv) Execute(conn *Conn, param string) { - addr := conn.PublicIp() + addr := conn.passiveListenIP() lastIdx := strings.LastIndex(addr, ":") if lastIdx <= 0 { conn.writeMessage(425, "Data connection failed") @@ -367,7 +367,9 @@ func (cmd commandList) Execute(conn *Conn, param string) { conn.writeMessage(550, err.Error()) return } + if !info.IsDir() { + conn.logger.Printf("%s is not a dir.\n", path) return } var files []FileInfo @@ -592,7 +594,8 @@ func (cmd commandPasv) RequireAuth() bool { } func (cmd commandPasv) Execute(conn *Conn, param string) { - socket, err := newPassiveSocket(conn.PublicIp(), conn.PassivePort(), conn.logger, conn.tlsConfig) + listenIP := conn.passiveListenIP() + socket, err := newPassiveSocket(listenIP, conn.PassivePort(), conn.logger, conn.tlsConfig) if err != nil { conn.writeMessage(425, "Data connection failed") return @@ -600,7 +603,7 @@ func (cmd commandPasv) Execute(conn *Conn, param string) { conn.dataConn = socket p1 := socket.Port() / 256 p2 := socket.Port() - (p1 * 256) - quads := strings.Split(conn.PublicIp(), ".") + quads := strings.Split(listenIP, ".") target := fmt.Sprintf("(%s,%s,%s,%s,%d,%d)", quads[0], quads[1], quads[2], quads[3], p1, p2) msg := "Entering Passive Mode " + target conn.writeMessage(227, msg) diff --git a/conn.go b/conn.go index 7e303d4..3023cc4 100644 --- a/conn.go +++ b/conn.go @@ -54,18 +54,29 @@ func (conn *Conn) PublicIp() string { return conn.server.PublicIp } +func (conn *Conn) passiveListenIP() string { + if len(conn.PublicIp()) > 0 { + return conn.PublicIp() + } + return conn.conn.LocalAddr().String() +} + func (conn *Conn) PassivePort() int { - portRange := strings.Split(conn.server.PassivePorts, "-") + if len(conn.server.PassivePorts) > 0 { + portRange := strings.Split(conn.server.PassivePorts, "-") - if len(portRange) != 2 { - log.Println("empty port") - return 0 - } + if len(portRange) != 2 { + log.Println("empty port") + return 0 + } - minPort, _ := strconv.Atoi(strings.TrimSpace(portRange[0])) - maxPort, _ := strconv.Atoi(strings.TrimSpace(portRange[1])) + minPort, _ := strconv.Atoi(strings.TrimSpace(portRange[0])) + maxPort, _ := strconv.Atoi(strings.TrimSpace(portRange[1])) - return minPort + mrand.Intn(maxPort-minPort) + return minPort + mrand.Intn(maxPort-minPort) + } + // let system automatically chose one port + return 0 } // returns a random 20 char string that can be used as a unique session ID