[TIL] 24.12.15 SUN

GDORI·2024년 12월 15일
0

TIL

목록 보기
133/143
post-thumbnail

IP 차단 리팩터링

하.. 분산서버로 컨버전 하는 동시에 nginx 리버스 프록시 사용으로 인해 게임서버로 접속하는 remoteAddress가 모두 nginx 서버 아이피로 찍히는 바람에 IP 밴을 담당하던 함수를 제거했었다.
그러니까 아주 그냥 자기 집 들리듯이 막 들어와 패킷을 엄청 쏴서 인스턴스가 멈추는 증상이 ...
그런데 현재 내가 알 수 있는 것은 nginx의 어떤 포트로 접근했는가, 이 점만 알 수 있었고 그 외의 다른 정보를 얻고자 하면 프록시 프로토콜을 사용해야했다.

프록시 프로토콜을 써야하나

프록시 프로토콜을 사용하게 되면 TCP 초반 패킷에 IP와 포트가 담겨서 패킷이 넘어오는데 지금 현재 게임서버의 패킷파서와는
결이 맞지 않은 친구라 한번 파싱을 하지 못한 데이터는 뒤에 정상적인 패킷도 읽지 못하는 문제점이 있었다.
별로 흥미가 생기지 않는 친구라 안쓰려 했건만.. 상시로 서버에 방문해주시는 봇님들과 공격자 분들의 성원으로 서버가 몇 번 터지는 바람에 사용을 결심했다.

프록시 프로토콜 ON


프록시 프로토콜을 허용하게 되면 초반 소켓이 연결되었을 때 위와 같이 패킷을 하나 툭 던져준다.
이 부분을 초반 파싱해주는 커스텀 모듈이 있는 것으로 알고있지만, 그리 어려운 부분이 아닐 것 같아 코드를 살짝 수정해준다.
근데 정확성을 위해 초반 패킷 파싱 후 PROXY로 시작하는지 확인해야 할텐데 정규표현식을 잘 모르는 관계로 찾아서 해보자..

/^PROXY (\S+) (\S+) (\S+) (\d+) (\d+)/

PROXY로 시작하는 문자열에서 3개의 문자열과 2개의 넘버를 추출할 수 있는 정규표현식이다.
어떻게 사용하냐?

const proxyData = data.toString().match(/^PROXY (\S+) (\S+) (\S+) (\d+) (\d+)/);

여기서 data는 onData로 들어온 패킷(청크)이다.
우리 서버에 적용하려면 클라이언트 소켓이 연결된 직후 초반 한번만 init으로 읽어주고 onData로 넘겨주면 될 것 같다.

바뀐 onConnection 코드

export const onConnection = (socket) => {
  try {
    const port = socket.remotePort;
    logger.info(`${port} - 포트로 연결시도 `);
    // 초기 소켓 설정
    socket.setNoDelay = true;
    socket.buffer = Buffer.alloc(0);
    socket.sequence = 0;
    socket.isAuthenticated = false; // 인증 상태 플래그 추가

    // 일정 시간 내에 인증 완료되지 않으면 연결 종료
    socket.authTimeout = setTimeout(() => {
      if (!socket.isAuthenticated) {
        console.log(`인증 시간 초과: ${port}`);
        socket.end();
      }
    }, 10 * 1000); // 10초 타임아웃

    socket.once('data', onInit(socket));
    socket.on('end', onEnd(socket));
    socket.on('error', onError(socket));
  } catch (err) {
    console.error('onConnection 오류', err);
    socket.end();
  }
};

onInit 코드

export const onInit = (socket) => (data) => {
  try {
    socket.buffer = Buffer.concat([socket.buffer, data]);

    // 프록시 프로토콜 헤더 초반과 끝 찾음
    const startIdx = socket.buffer.findIndex((byte) => byte === 'P'.charCodeAt(0));
    const endIdx = socket.buffer.findIndex((byte) => byte === '\n'.charCodeAt(0));

    // 충족될 시 아래 조건문 실행
    if (startIdx !== -1 && endIdx !== -1) {
      const proxyData = socket.buffer.slice(startIdx, endIdx + 1).toString();
      // 정규표현식을 통해 데이터 추출
      const match = proxyData.match(/^PROXY (\S+) (\S+) (\S+) (\d+) (\d+)/);
      if (match) {
        socket.userIp = match[2];
        socket.userPort = match[4];

        console.log(`${socket.userIp} : ${socket.userPort} - 접속됨`);

        // 헤더 처리 후 버퍼에서 해당 부분 제거
        socket.buffer = socket.buffer.slice(endIdx + 1);
      }

      // 초기화 작업 끝났으니 onData핸들러로 변경
      socket.on('data', onData(socket));

      // 남아있는 데이터가 있으면 새 핸들러로 전달
      if (socket.buffer.length > 0) {
        socket.emit('data', socket.buffer);
        socket.buffer = Buffer.alloc(0);
      }
    }
  } catch (err) {
    handleErr(socket, err);
  }
};

이제 아이피가 잘 찍히는군

그렇다면, 올바르지 않은 패킷을 보내는 클라이언트를 IP 밴을 먹이면 될 것 같은데..
솔직히 게임서버까지도 오게끔 하고 싶지 않다.
api를 통해 nginx 헬퍼서버에 밴을 요청해서 nginx 단에서 오지 못하게 막아야겠다.

nginx 헬퍼 서버 처리부분은 포스트가 길어지니 추후 따로 포스팅 하도록 하고..

간단하게 함수 하나 만들기

export const requestIpBan = async (ip) => {
  try {
    logger.info(`${ip} - Block request`);
    const url = config.server.nginx;
    const key = config.auth.api_key;
    const response = await fetch(url, {
      method: 'POST', // HTTP 메서드: POST
      headers: {
        'Content-Type': 'application/json', // JSON 데이터 전송
        authorization: key,
      },
      body: JSON.stringify({ ip }), // 데이터를 JSON 문자열로 변환하여 전송
    });

    if (!response.ok) {
      throw new Error('nginx 서버 요청 오류');
    }
  } catch (err) {
    logger.warn(err.message);
  }
};

nginx에 아이피 밴을 요청하는 함수이다. 얘를 이제 필요한 부분에 가져다 쓰면 된다.

onConnection 부분 추가

    // 일정 시간 내에 인증 완료되지 않으면 연결 종료
    socket.authTimeout = setTimeout(async () => {
      if (!socket.isAuthenticated) {
        if (socket.userIp) {
          console.log(`인증 시간 초과: ${socket.userIp}`);
          // 인증을 못했다는 것은 비정상 루트로 들어왔다는 내용이기에 차단
          await requestIpBan(socket.userIp);
        }
        socket.end();
        socket.destroy();
      }
    }, 10 * 1000); // 10초 타임아웃

게임 서버에 들어온 클라이언트는 정상적인 루트로 들어왔다면 접속과 동시에 인증 패킷을 보내게 되어있다.
따라서, 정상 루트가 아닌 사용자라면 인증을 할 수 없다는 것.
고로 밴.

패킷 처리부에도 추가

export const recvPacket = (socket, packet) => {
  if (!packet) {
    requestIpBan(socket.userIp);
    socket.end();
    socket.destroy();
    throw new Error('receivePacket 인자값 확인하세요.');
  }
  packetManager.enQueueRecv(socket, packet);
};

정상적인 패킷이면 조건문에 해당하지 않기 때문에 밴처리 추가

이 지독한 친구들 다 막으려면 시행착오가 좀 필요할 것 같다.
추후에 nginx 헬퍼서버 단에서 주기적으로 로그 체크해서 문자열 보고 바로 밴하는 것도 추가 해야겠다.

profile
하루 최소 1시간이라도 공부하자..

0개의 댓글