diff --git a/FluentFTP.CSharpExamples/LocalIpAddress.cs b/FluentFTP.CSharpExamples/LocalIpAddress.cs new file mode 100644 index 000000000..7a6b8fb3a --- /dev/null +++ b/FluentFTP.CSharpExamples/LocalIpAddress.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CSharpExamples +{ + using System.Net; + using System.Net.Sockets; + + using FluentFTP; + + public static class LocalIpAddress + { + public static void LocaIpAddressExample() + { + // IP addresses for current host inside myprivatedomain + var localIpAddresses = new[] + { + IPAddress.Parse("10.244.191.143"), + IPAddress.Parse("fcec:177:cfbd:6555:8f8c::1") + }; + + foreach (var localIpAddress in localIpAddresses) + { + // let's say that ftp.myprivatedomain has ipv4 and ipv5 addresses + using (var f = new FtpClient("ftp.myprivatedomain", "test", "test") + { + InternetProtocolVersions = localIpAddress.AddressFamily == AddressFamily.InterNetworkV6 ? FtpIpVersion.IPv6 : FtpIpVersion.IPv4, + + // Equivalent to lftp's ftp:port-ipv[4|6] and net:socket-bind-ipv[4|6] (see http://manpages.org/lftp) + LocalIpAddress = localIpAddress + }) + { + f.Connect(); + + Console.WriteLine($"Connected to {f.RemoteEndPoint} from {f.LocalEndPoint}"); + foreach (var file in f.GetListing()) + { + Console.Out.WriteLine(file); + } + + f.Disconnect(); + } + } + } + } +} diff --git a/FluentFTP/Client/FtpClient_Connection.cs b/FluentFTP/Client/FtpClient_Connection.cs index ac8f42567..c4320c1ca 100644 --- a/FluentFTP/Client/FtpClient_Connection.cs +++ b/FluentFTP/Client/FtpClient_Connection.cs @@ -310,6 +310,7 @@ protected FtpClient CloneConnection() { conn.ServerHandler = ServerHandler; conn.UploadDirectoryDeleteExcluded = UploadDirectoryDeleteExcluded; conn.DownloadDirectoryDeleteExcluded = DownloadDirectoryDeleteExcluded; + conn.LocalIpAddress = LocalIpAddress; // configure new connection as clone of self (.NET core props only) #if CORE @@ -359,7 +360,7 @@ public virtual void Connect() { } if (m_stream == null) { - m_stream = new FtpSocketStream(m_SslProtocols); + m_stream = new FtpSocketStream(m_SslProtocols, m_LocalIpAddress); m_stream.Client = this; m_stream.ValidateCertificate += new FtpSocketStreamSslValidation(FireValidateCertficate); } @@ -517,7 +518,7 @@ public virtual void Connect() { } if (m_stream == null) { - m_stream = new FtpSocketStream(m_SslProtocols); + m_stream = new FtpSocketStream(m_SslProtocols, m_LocalIpAddress); m_stream.Client = this; m_stream.ValidateCertificate += new FtpSocketStreamSslValidation(FireValidateCertficate); } diff --git a/FluentFTP/Client/FtpClient_Properties.cs b/FluentFTP/Client/FtpClient_Properties.cs index 6fa96ea8f..d7c039f4b 100644 --- a/FluentFTP/Client/FtpClient_Properties.cs +++ b/FluentFTP/Client/FtpClient_Properties.cs @@ -1042,6 +1042,20 @@ public string SendHostDomain { set => m_SendHostDomain = value; } + private IPAddress m_LocalIpAddress; + /// + /// Allow to bind sockets to a particular local IP/interface. + /// Useful if you have several usable public IP addresses and want to use a particular one. + /// + public IPAddress LocalIpAddress + { + get => m_LocalIpAddress; + set => m_LocalIpAddress = value; + } + + public IPEndPoint LocalEndPoint => this.m_stream.LocalEndPoint; + public IPEndPoint RemoteEndPoint => this.m_stream.RemoteEndPoint; + // ADD PROPERTIES THAT NEED TO BE CLONED INTO // FtpClient.CloneConnection() } diff --git a/FluentFTP/Helpers/LocalPorts.cs b/FluentFTP/Helpers/LocalPorts.cs new file mode 100644 index 000000000..b9cbd13ea --- /dev/null +++ b/FluentFTP/Helpers/LocalPorts.cs @@ -0,0 +1,61 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// +// +// +// The local ports. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace FluentFTP.Helpers +{ + #region Usings + + using System; + using System.Collections.Generic; + using System.Linq; + using System.Net; + using System.Net.NetworkInformation; + + #endregion + + /// + /// The local ports. + /// + public static class LocalPorts + { + /// + /// The r. + /// + internal static readonly Random R = new Random(); + + /// + /// Get available. + /// + /// + /// The local ip address. + /// + /// + /// The . + /// + public static int GetRandomAvailable(IPAddress localIpAddress) + { + lock (R) + { + var ipGlobalProperties = IPGlobalProperties.GetIPGlobalProperties(); + var tcpConnInfoArray = ipGlobalProperties.GetActiveTcpListeners(); + var inUsePorts = new HashSet( + tcpConnInfoArray.Where(ipEndPoint => localIpAddress.Equals(ipEndPoint.Address)) + .Select(ipEndPoint => ipEndPoint.Port)); + int localPort; + do + { + localPort = 1025 + R.Next(32000); + } + while (inUsePorts.Contains(localPort)); + + return localPort; + } + } + } +} \ No newline at end of file diff --git a/FluentFTP/Streams/FtpDataStream.cs b/FluentFTP/Streams/FtpDataStream.cs index 936998d98..6fdbe9739 100644 --- a/FluentFTP/Streams/FtpDataStream.cs +++ b/FluentFTP/Streams/FtpDataStream.cs @@ -150,7 +150,7 @@ public void SetPosition(long pos) { /// Creates a new data stream object /// /// The control connection to be used for carrying out this operation - public FtpDataStream(FtpClient conn) : base(conn.SslProtocols) { + public FtpDataStream(FtpClient conn) : base(conn.SslProtocols, conn.LocalIpAddress) { if (conn == null) { throw new ArgumentException("The control connection cannot be null."); } diff --git a/FluentFTP/Streams/FtpSocketStream.cs b/FluentFTP/Streams/FtpSocketStream.cs index 0d77550ab..c8b61a769 100644 --- a/FluentFTP/Streams/FtpSocketStream.cs +++ b/FluentFTP/Streams/FtpSocketStream.cs @@ -9,22 +9,28 @@ using System.ComponentModel; using System.Diagnostics; using System.Net; +using System.Net.NetworkInformation; + +using FluentFTP.Helpers; using FluentFTP.Exceptions; #if CORE || NET45 using System.Threading.Tasks; - #endif namespace FluentFTP { + + /// /// Stream class used for talking. Used by FtpClient, extended by FtpDataStream /// public class FtpSocketStream : Stream, IDisposable { private SslProtocols m_SslProtocols; + private IPAddress m_LocalIpAddress; public FtpClient Client; - public FtpSocketStream(SslProtocols defaultSslProtocols) { + public FtpSocketStream(SslProtocols defaultSslProtocols, IPAddress localIpAddress) { m_SslProtocols = defaultSslProtocols; + m_LocalIpAddress = localIpAddress; } /// @@ -815,7 +821,7 @@ public void SetSocketOption(SocketOptionLevel level, SocketOptionName name, bool /// Internet Protocol versions to support during the connection phase public void Connect(string host, int port, FtpIpVersion ipVersions) { #if CORE - IPAddress[] addresses = Dns.GetHostAddressesAsync(host).Result; + IPAddress[] addresses = Dns.GetHostAddresses(host); #else IAsyncResult ar = null; var addresses = Dns.GetHostAddresses(host); @@ -861,10 +867,14 @@ public void Connect(string host, int port, FtpIpVersion ipVersions) { } m_socket = new Socket(addresses[i].AddressFamily, SocketType.Stream, ProtocolType.Tcp); + BindLocalIfNecessary(); + #if CORE + + var args = new SocketAsyncEventArgs { - RemoteEndPoint = new IPEndPoint(addresses[i], port) - }; + RemoteEndPoint = new IPEndPoint(addresses[i], port) + }; var connectEvent = new ManualResetEvent(false); args.Completed += (s, e) => { connectEvent.Set(); }; @@ -968,6 +978,7 @@ public async Task ConnectAsync(string host, int port, FtpIpVersion ipVersions, C } m_socket = new Socket(addresses[i].AddressFamily, SocketType.Stream, ProtocolType.Tcp); + BindLocalIfNecessary(); #if CORE await EnableCancellation(m_socket.ConnectAsync(addresses[i], port), token, () => CloseSocket()); break; @@ -1305,6 +1316,22 @@ public void EndAccept(IAsyncResult ar) { } } #endif + private void BindLocalIfNecessary() + { + if (this.m_LocalIpAddress == null) + { + return; + } + + var localPort = LocalPorts.GetRandomAvailable(m_LocalIpAddress); + var localEndpoint = new IPEndPoint(m_LocalIpAddress, localPort); + +#if DEBUG + Client.LogStatus(FtpTraceLevel.Verbose, $"Will now bind to {localEndpoint}"); +#endif + + this.m_socket.Bind(localEndpoint); + } #if CORE internal SocketAsyncEventArgs BeginAccept() {