A lightweight, modern FTP server library for .NET.
- Fully asynchronous operation.
- Supports both Dependency Injection and manual instantiation.
- Extensible authentication and file system providers.
- Built-in support for common FTP commands (USER, PASS, LIST, RETR, STOR, etc.).
- PASV mode support.
dotnet add package FmiSrl.FtpServer.ServerTo use the FTP server in a modern .NET application (e.g., ASP.NET Core, Worker Service), register it in your Program.cs:
using Microsoft.Extensions.DependencyInjection;
using FmiSrl.FtpServer.Server;
using FmiSrl.FtpServer.Server.DependencyInjection;
using FmiSrl.FtpServer.Server.Services;
var builder = Host.CreateApplicationBuilder(args);
// Configure Options for Providers
builder.Services.Configure<PhysicalFileSystemProviderOptions>(opt =>
{
opt.RootDirectory = "./my_ftp_root";
});
builder.Services.Configure<SimpleAuthenticationProviderOptions>(opt =>
{
opt.Username = "admin";
opt.Password = "secure_password";
});
// Add FTP Server and configure providers via generic DI
builder.Services.AddFtpServer(options =>
{
options.FtpPort = 21;
options.ServerName = "My Custom FTP Server";
})
.UseFileSystemProvider<PhysicalFileSystemProvider>()
.UseAuthenticationProvider<SimpleAuthenticationProvider>();
var host = builder.Build();
// Get the server instance and start it
var ftpServer = host.Services.GetRequiredService<FtpServer>();
await ftpServer.StartAsync();
await host.RunAsync();The server is registered as a Singleton by default.
You can also use the library in simple console applications or legacy projects without a DI container:
using FmiSrl.FtpServer.Server;
using FmiSrl.FtpServer.Server.Services;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
var serverOptions = Options.Create(new FtpServerConfigurationOptions
{
FtpPort = 2121,
ServerName = "Standalone FTP Server"
});
var fsOptions = Options.Create(new PhysicalFileSystemProviderOptions
{
RootDirectory = "./ftp_root"
});
var authOptions = Options.Create(new SimpleAuthenticationProviderOptions
{
Username = "user",
Password = "password"
});
// Providers are strictly required
var ftpServer = new FtpServer(
new PhysicalFileSystemProvider(fsOptions),
new SimpleAuthenticationProvider(authOptions),
Enumerable.Empty<IFtpCommandMiddleware>(),
Enumerable.Empty<IFtpServerEventHandler>(),
serverOptions,
NullLogger<FtpServer>.Instance
);
await ftpServer.StartAsync();
Console.WriteLine("Server started. Press any key to stop.");
Console.ReadKey();
await ftpServer.StopAsync();Every PASV command binds one listener from the configured range, and holds that port until the transfer ends, the client disconnects, or the idle timeout expires:
var serverOptions = Options.Create(new FtpServerConfigurationOptions
{
PasvMinPort = 50000,
PasvMaxPort = 50100,
PasvIdleTimeout = TimeSpan.FromMinutes(2)
});Size the range for the peak number of concurrent transfers, with headroom for ports still in
TIME_WAIT after a download. Ports are handed out round-robin, so a port that was just released is
the last one retried.
PasvIdleTimeout bounds how long a passive listener waits for the client to dial in. Clients that
issue PASV and then never connect — aborted transfers, dropped networks — are the usual cause of a
drained range; the timeout reclaims their ports. It is disarmed the moment the data connection is
accepted, so it never cuts a slow transfer short. Set it to Timeout.InfiniteTimeSpan to wait
forever.
Setting PasvMinPort and PasvMaxPort to 0 disables the range entirely and lets the operating
system assign an ephemeral port per data connection, which is only appropriate when no firewall or
NAT rule constrains the passive ports. When a configured range runs dry the server falls back to an
ephemeral port and logs a warning — that port will usually be blocked by a firewall rule scoped to
the range, so the warning is worth alerting on.
The library uses Microsoft.Extensions.Logging. When using DI, it will automatically use the configured logging providers (e.g., Serilog, Console, Debug).
When used manually, you can pass any ILogger<FtpServer> implementation to the constructor.
Implement the IAuthenticationProvider interface to customize how users are authenticated:
public interface IAuthenticationProvider
{
Task<bool> AuthenticateAsync(string username, string password);
}Implement the IFileSystemProvider interface to provide custom storage (e.g., Azure Blob Storage, Database, S3).
It provides an FtpAuthenticationContext which includes the currently authenticated username:
using FmiSrl.FtpServer.Server.Abstractions;
public interface IFileSystemProvider
{
Task<IEnumerable<FileSystemEntry>> GetEntriesAsync(FtpAuthenticationContext authContext, string path);
Task<Stream> OpenReadAsync(FtpAuthenticationContext authContext, string path);
Task<Stream> OpenWriteAsync(FtpAuthenticationContext authContext, string path);
Task DeleteFileAsync(FtpAuthenticationContext authContext, string path);
Task CreateDirectoryAsync(FtpAuthenticationContext authContext, string path);
Task DeleteDirectoryAsync(FtpAuthenticationContext authContext, string path);
Task<bool> FileExistsAsync(FtpAuthenticationContext authContext, string path);
Task<bool> DirectoryExistsAsync(FtpAuthenticationContext authContext, string path);
Task RenameAsync(FtpAuthenticationContext authContext, string oldPath, string newPath);
}