42. 프록시 뒤 클라이언트 주소 식별: 신뢰 경계와 스트림 경계 (C#)

난이도 중 해설 보기 →

결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커 (A)(B) 는 주목 위치 힌트다.

결함 코드 · C#
// [시나리오]
// 게임 로그인 서버가 L4 로드밸런서 뒤에 배치되어 있다. LB 는 TCP 연결을 프록시하며,
// 원래 클라이언트의 주소를 전달하기 위해 연결 시작 시 PROXY protocol 헤더를 한 번 보낸다.
// 그 뒤로는 클라이언트가 보낸 바이트가 그대로 흐른다.
//
// 서버는 클라이언트 IP 를 기준으로 다음을 수행한다.
//   - 접속 레이트 리밋(초당 연결 수 제한)
//   - IP 밴 목록 대조
//   - 지역(region) 판별 및 접속 로그 기록
//
// 운영 중 다음 현상이 보고되었다.
//   - 특정 시간대에 정상 유저들이 대량으로 "접속 제한"에 걸린다.
//   - IP 밴이 사실상 동작하지 않는다.
//   - 접속 로그의 IP 가 몇 개 값으로만 채워진다.
//
// [요구사항]
// - 레이트 리밋/밴/로그는 실제 클라이언트 IP 를 기준으로 해야 한다.
// - 프록시가 알려주는 주소 정보는 신뢰할 수 있는 출처에서 온 것만 채택해야 한다.
//
// [과제]
// (A), (B) 지점을 중심으로 위 현상이 발생하는 조건을 찾고, 재현 시나리오와 수정안을
// 제시하시오.

using System;
using System.Collections.Concurrent;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;

namespace GameServer.Session
{
    public sealed class ConnectionInfo
    {
        public IPAddress ClientAddress;
        public string Region;
    }

    public sealed class LoginAcceptor
    {
        private readonly ConcurrentDictionary<IPAddress, RateCounter> _rate = new();
        private readonly BanList _bans = new();

        public async Task OnAcceptedAsync(Socket socket)
        {
            var info = new ConnectionInfo();

            // (A)
            var peer = (IPEndPoint)socket.RemoteEndPoint;
            info.ClientAddress = peer.Address;

            var buffer = new byte[256];
            int n = await socket.ReceiveAsync(buffer, SocketFlags.None);
            string first = Encoding.ASCII.GetString(buffer, 0, n);

            // (B)
            if (first.StartsWith("PROXY "))
            {
                // "PROXY TCP4 <src> <dst> <sport> <dport>\r\n"
                var parts = first.Split(' ');
                info.ClientAddress = IPAddress.Parse(parts[2]);
            }

            if (_bans.IsBanned(info.ClientAddress))
            {
                socket.Close();
                return;
            }

            var counter = _rate.GetOrAdd(info.ClientAddress, _ => new RateCounter());
            if (counter.Increment() > 10)
            {
                socket.Close();
                return;
            }

            info.Region = GeoIp.Lookup(info.ClientAddress);
            Log(info);
            await HandleSessionAsync(socket, info);
        }

        private void Log(ConnectionInfo info) { /* ... */ }
        private Task HandleSessionAsync(Socket s, ConnectionInfo i) => Task.CompletedTask;
    }

    public sealed class RateCounter { public int Increment() => 0; }
    public sealed class BanList { public bool IsBanned(IPAddress a) => false; }
    public static class GeoIp { public static string Lookup(IPAddress a) => "KR"; }
}
내 리뷰 · C#
내 답안 · 자동 저장

작성 후 위 해설 보기에서 모범 해설과 대조하세요.