Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ftp.go
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,7 @@ func (c *ServerConn) pasv() (host string, port int, err error) {
func isBogusDataIP(cmdIP, dataIP net.IP) bool {
// Logic stolen from lftp (https://github.com/lavv17/lftp/blob/d67fc14d085849a6b0418bb3e912fea2e94c18d1/src/ftpclass.cc#L769)
return dataIP.IsMulticast() ||
dataIP.IsUnspecified() || // Explicitly handle 0.0.0.0 and ::
cmdIP.IsPrivate() != dataIP.IsPrivate() ||
cmdIP.IsLoopback() != dataIP.IsLoopback()
}
Expand Down
2 changes: 1 addition & 1 deletion parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ var listTests = []line{
{"08-10-15 02:04PM <DIR> Billing", "Billing", 0, EntryTypeFolder, newTime(2015, time.August, 10, 14, 4)},
{"08-07-2015 07:50PM 718 Post_PRR_20150901_1166_265118_13049.dat", "Post_PRR_20150901_1166_265118_13049.dat", 718, EntryTypeFile, newTime(2015, time.August, 7, 19, 50)},
{"08-10-2015 02:04PM <DIR> Billing", "Billing", 0, EntryTypeFolder, newTime(2015, time.August, 10, 14, 4)},

// dir and file names that contain multiple spaces
{"drwxr-xr-x 3 110 1002 3 Dec 02 2009 spaces dir name", "spaces dir name", 0, EntryTypeFolder, newTime(2009, time.December, 2)},
{"-rwxr-xr-x 3 110 1002 1234567 Dec 02 2009 file name", "file name", 1234567, EntryTypeFile, newTime(2009, time.December, 2)},
Expand Down
92 changes: 92 additions & 0 deletions pasv_integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package ftp

import (
"net"
"net/textproto"
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// TestPASVZeroIPIntegration tests the complete PASV flow when server returns 0,0,0,0
func TestPASVZeroIPIntegration(t *testing.T) {
// Create a mock server that returns 0,0,0,0 in PASV
l, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
defer l.Close()

serverAddr := l.Addr().String()

// Start a simple FTP server that returns 0,0,0,0 in PASV
go func() {
conn, err := l.Accept()
if err != nil {
return
}
defer conn.Close()

proto := textproto.NewConn(conn)
defer proto.Close()

// Send welcome message
proto.PrintfLine("220 Test FTP Server")

// Handle commands
for {
line, err := proto.ReadLine()
if err != nil {
break
}

parts := strings.Fields(line)
if len(parts) == 0 {
continue
}

cmd := strings.ToUpper(parts[0])
switch cmd {
case "USER":
proto.PrintfLine("331 User name okay, need password")
case "PASS":
proto.PrintfLine("230 User logged in, proceed")
case "FEAT":
proto.PrintfLine("211-Features:\r\n PASV\r\n EPSV\r\n211 End")
case "TYPE":
proto.PrintfLine("200 Type set")
case "OPTS":
proto.PrintfLine("200 Command okay")
case "PASV":
// Return 0,0,0,0 as IP with a dummy port
proto.PrintfLine("227 Entering Passive Mode (0,0,0,0,20,21).")
case "QUIT":
proto.PrintfLine("221 Goodbye")
return
default:
proto.PrintfLine("500 Unknown command")
}
}
}()

// Connect to our test server
c, err := Dial(serverAddr)
require.NoError(t, err)
defer c.Quit()

err = c.Login("test", "test")
require.NoError(t, err)

// Disable EPSV to force PASV
c.options.disableEPSV = true

// Test getDataConnPort which internally calls pasv()
host, port, err := c.getDataConnPort()
require.NoError(t, err)

// Should return 127.0.0.1 (the connection IP) instead of 0.0.0.0
assert.Equal(t, "127.0.0.1", host, "Should use connection IP when PASV returns 0,0,0,0")
assert.Equal(t, 5141, port, "Should return correct port (20*256 + 21)")

t.Logf("PASV with 0,0,0,0 correctly returned host=%s, port=%d", host, port)
}
72 changes: 72 additions & 0 deletions pasv_zero_ip_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package ftp

import (
"net"
"testing"

"github.com/stretchr/testify/assert"
)

// TestBogusDataIPWithUnspecified tests that 0.0.0.0 and :: are treated as bogus
func TestBogusDataIPWithUnspecified(t *testing.T) {
tests := []struct {
name string
cmdIP string
dataIP string
expected bool
}{
{
name: "0.0.0.0 should be bogus",
cmdIP: "127.0.0.1",
dataIP: "0.0.0.0",
expected: true,
},
{
name: "IPv6 unspecified should be bogus",
cmdIP: "::1",
dataIP: "::",
expected: true,
},
{
name: "0.0.0.0 from any IP should be bogus",
cmdIP: "192.168.1.1",
dataIP: "0.0.0.0",
expected: true,
},
{
name: "Same private IP should not be bogus",
cmdIP: "192.168.1.1",
dataIP: "192.168.1.2",
expected: false,
},
{
name: "Same loopback IP should not be bogus",
cmdIP: "127.0.0.1",
dataIP: "127.0.0.1",
expected: false,
},
{
name: "Multicast should be bogus",
cmdIP: "127.0.0.1",
dataIP: "224.0.0.1",
expected: true,
},
{
name: "Different network types should be bogus",
cmdIP: "127.0.0.1", // loopback
dataIP: "192.168.1.1", // private
expected: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmdIP := net.ParseIP(tt.cmdIP)
dataIP := net.ParseIP(tt.dataIP)

result := isBogusDataIP(cmdIP, dataIP)
assert.Equal(t, tt.expected, result,
"isBogusDataIP(%s, %s) should return %v", tt.cmdIP, tt.dataIP, tt.expected)
})
}
}