diff --git a/ftp.go b/ftp.go
index 02136ae..b92cd1f 100644
--- a/ftp.go
+++ b/ftp.go
@@ -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()
}
diff --git a/parse_test.go b/parse_test.go
index f53a61e..080ca59 100644
--- a/parse_test.go
+++ b/parse_test.go
@@ -70,7 +70,7 @@ var listTests = []line{
{"08-10-15 02:04PM
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 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)},
diff --git a/pasv_integration_test.go b/pasv_integration_test.go
new file mode 100644
index 0000000..2874af4
--- /dev/null
+++ b/pasv_integration_test.go
@@ -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)
+}
diff --git a/pasv_zero_ip_test.go b/pasv_zero_ip_test.go
new file mode 100644
index 0000000..0422be5
--- /dev/null
+++ b/pasv_zero_ip_test.go
@@ -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)
+ })
+ }
+}