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++
// [시나리오]
// C++ 로그인 서버가 L4 로드밸런서 뒤에 있다. LB 는 TCP 연결을 프록시하며 연결 시작 시
// PROXY protocol 헤더를 한 번 보낸 뒤, 이후로는 클라이언트 바이트를 그대로 중계한다.
//
// 서버는 클라이언트 IP 기준으로 접속 레이트 리밋, IP 밴 대조, 지역 판별/로그를 수행한다.
//
// 운영 중 보고된 현상:
// - 특정 시간대에 정상 유저들이 대량으로 "접속 제한"에 걸린다.
// - IP 밴이 사실상 동작하지 않는다.
// - 접속 로그의 IP 가 몇 개 값으로만 채워진다.
//
// [요구사항]
// - 레이트 리밋/밴/로그는 실제 클라이언트 IP 기준.
// - 프록시가 알려주는 주소 정보는 신뢰할 수 있는 출처에서 온 것만 채택.
//
// [과제]
// (A), (B) 지점을 중심으로 위 현상이 발생하는 조건을 찾고, 재현 시나리오와 수정안을
// 제시하시오.
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#include <cstring>
#include <sstream>
#include <string>
#include <unordered_map>
namespace gs {
struct ConnectionInfo {
std::string clientAddress;
std::string region;
};
class RateCounter { public: int increment() { return ++n_; } private: int n_ = 0; };
class BanList { public: bool isBanned(const std::string&) const { return false; } };
std::string geoIpLookup(const std::string&) { return "KR"; }
class LoginAcceptor {
public:
void onAccepted(int fd) {
ConnectionInfo info;
// (A)
sockaddr_in peer{};
socklen_t plen = sizeof(peer);
::getpeername(fd, reinterpret_cast<sockaddr*>(&peer), &plen);
char ipbuf[INET_ADDRSTRLEN] = {0};
::inet_ntop(AF_INET, &peer.sin_addr, ipbuf, sizeof(ipbuf));
info.clientAddress = ipbuf;
char buffer[256];
ssize_t n = ::recv(fd, buffer, sizeof(buffer), 0);
std::string first(buffer, n > 0 ? static_cast<std::size_t>(n) : 0);
// (B)
if (first.rfind("PROXY ", 0) == 0) {
// "PROXY TCP4 <src> <dst> <sport> <dport>\r\n"
std::istringstream iss(first);
std::string tag, proto, src;
iss >> tag >> proto >> src;
info.clientAddress = src;
}
if (bans_.isBanned(info.clientAddress)) { ::close(fd); return; }
if (rate_[info.clientAddress].increment() > 10) { ::close(fd); return; }
info.region = geoIpLookup(info.clientAddress);
log(info);
handleSession(fd, info);
}
private:
void log(const ConnectionInfo&) {}
void handleSession(int, const ConnectionInfo&) {}
std::unordered_map<std::string, RateCounter> rate_;
BanList bans_;
};
} // namespace gs
int main() { return 0; } 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.