简体   繁体   中英

NET6.0 FTP server networking issue on Docker container

I'm developing a NET 6.0 FTP server as part of a functionality to load the firmware of a hardware device. I need to have it inside a Docker container but I'm unable to make it work on that environment when it works perfectly when I execute it as a regular executable. It seems to be something related to docker networking but I can't figure it out what it is.

This is the Dockerfile for the container, that is based on Alpine (mcr.microsoft.com/dotnet/aspnet:6.0-alpine), with some additions from the default Dockerfile created by Visual Studio:

FROM mcr.microsoft.com/dotnet/aspnet:6.0-alpine AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
  
RUN apk add openrc --no-cache

ENV MUSL_LOCALE_DEPS cmake make musl-dev gcc gettext-dev libintl 
ENV MUSL_LOCPATH /usr/share/i18n/locales/musl

RUN apk add --no-cache \
    $MUSL_LOCALE_DEPS \
    && wget https://gitlab.com/rilian-la-te/musl-locales/-/archive/master/musl-locales-master.zip \
    && unzip musl-locales-master.zip \
      && cd musl-locales-master \
      && cmake -DLOCALE_PROFILE=OFF -D CMAKE_INSTALL_PREFIX:PATH=/usr . && make && make install \
      && cd .. && rm -r musl-locales-master

RUN apk add icu-libs
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false

FROM mcr.microsoft.com/dotnet/sdk:6.0-alpine AS build
WORKDIR /src
COPY ["nuget.config", "."]
COPY ["CONTAINERS/Project1/Project1.csproj", "CONTAINERS/Project/"]
RUN dotnet restore "CONTAINERS/Project1.csproj"
COPY . .
WORKDIR "/src/CONTAINERS/Project1"
RUN dotnet build "Project1.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "Project1.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Project1.dll"]

The Docker run parameters are these

-p 20:20 -p 21000-22000:21000-22000

where 20 is the control port for FTP, it is the port used by that external hardware device and cannot be modified by me, and 21000-22000 is the range for FTP passive mode.

The FTP server code is quite simple and it works nice directly being executed in the host machine:

public class FtpServer : IDisposable
{
    ...
    
    public ErrorCode Start(string ip, int port, string basepath, string user, string password, int minPassivePort = 0, int maxPassivePort = 0)
    {
        ErrorCode retVal = ErrorCode.Success;

        _basepath = basepath;

        _user = user;

        _password = password;

        PassivePortMin = minPassivePort;
        PassivePortMax = maxPassivePort;

        ServicePointManager.DefaultConnectionLimit = 200;

        _localEndPoint = new IPEndPoint(IPAddress.Parse(ip), port);

        _listener = new TcpListener(_localEndPoint);

        _listening = true;

        _activeConnections = new List<ClientConnection>();

        try
        {
            _listener.Start();

            LocalEndpoint = ((IPEndPoint)_listener.LocalEndpoint).Address.ToString();

            _listener.BeginAcceptTcpClient(HandleAcceptTcpClient, _listener);
        }
        catch (Exception ex)
        {
            log.Error("Error starting FTP server", ex);

            retVal = ErrorCode.ConnectionFailure;
        }

        return retVal;
    }

    private void HandleAcceptTcpClient(IAsyncResult result)
    {
        if (_listening)
        {
            TcpClient client = _listener.EndAcceptTcpClient(result);

            _listener.BeginAcceptTcpClient(HandleAcceptTcpClient, _listener);

            ClientConnection connection = new ClientConnection(client, _user, _password, _basepath);

            ThreadPool.QueueUserWorkItem(connection.HandleClient, client);
        }
    }

public class ClientConnection
{
        public ClientConnection(TcpClient client, string username, string password, string basepath)
    {
        
        _controlClient = client;

        _currentUser = new User
        {
            Username = username,
            Password = password,
            HomeDir = basepath
        };

        _validCommands = new List<string>();
    }

    public void HandleClient(object obj)
    {
        //  bool error = false;

        try
        {
            _remoteEndPoint = (IPEndPoint)_controlClient.Client.RemoteEndPoint;

            _clientIP = _remoteEndPoint.Address.ToString();

            _controlStream = _controlClient.GetStream();

            _controlReader = new StreamReader(_controlStream);
            _controlWriter = new StreamWriter(_controlStream);
            
            _controlWriter.WriteLine("220 Service Ready.");
            _controlWriter.Flush();

            _validCommands.AddRange(new string[] { "AUTH", "USER", "PASS", "QUIT", "HELP", "NOOP" });

            string line;

            _dataClient = new TcpClient();

            string renameFrom = null;

            while ((line = _controlReader.ReadLine()) != null)
            {
                string response = null;

                string[] command = line.Split(' ');

                string cmd = command[0].ToUpperInvariant();
                string arguments = command.Length > 1 ? line.Substring(command[0].Length + 1) : null;

                if (arguments != null && arguments.Trim().Length == 0)
                {
                    arguments = null;
                }

                if (!_validCommands.Contains(cmd))
                {
                    response = CheckUser();
                }

                if (cmd != "RNTO")
                {
                    renameFrom = null;
                }

                Console.WriteLine(cmd + " " + arguments);

                if (response == null)
                {
                    switch (cmd)
                    {

                        default:
                            response = "502 Command not implemented";
                            break;
                    }
                }

                if (_controlClient == null || !_controlClient.Connected)
                {
                    break;
                }
                else
                {
                    if (!string.IsNullOrEmpty(response))
                    {
                        _controlWriter.WriteLine(response);
                        _controlWriter.Flush();
                    }

                    Console.WriteLine(response);

                    if (response.StartsWith("221"))
                    {
                        break;
                    }
                }
            }

        }
        catch (Exception ex)
        {
            log.Error("Error sending command", ex);
            Console.WriteLine(ex.Message);
            Console.WriteLine(ex.StackTrace);
        }

        Dispose();
    }

}

The issue seems to be located in _controlWriter, it seems that anything is blocking the response to the device (220 Service Ready) or maybe the frame is not being redirected to the right network interface, because nothing is read from _controlReader. As I mentioned earlier, this exact same code works perfectly when I execute it in the host machine, outside Docker container, that's the reason why I think it could be something related to Docker networking.

I hope you can help me, thanks!

It was something related to carriage return. Since docker container used a Linux based image, the carriage return was \n and the device expected \r\n.

Thanks to everyone who took a look at this.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM