데이터 수집 | TAP(미러링) 환경에서 syslog 수집 안될 때 해결 방법 (tcpdump → Python RAW Socket까지)
본문 바로가기

Splunk/Splunk Project

데이터 수집 | TAP(미러링) 환경에서 syslog 수집 안될 때 해결 방법 (tcpdump → Python RAW Socket까지)

728x90
반응형

이번 작업은 TAP 방식으로 미러링된 트래픽을 받아서,

그 안에 포함된 UDP 514 syslog 메시지를 추출하고, 원본 장비 IP 기준으로 파일에 저장하는 구조를 만드는 과정이었다.

 

처음에는 단순히 syslog-ng가 받아줄 것이라고 생각했지만,

실제로는 미러링된 패킷은 애플리케이션 소켓 레벨로 자연스럽게 올라오지 않기 때문에 syslog-ng가 반응하지 않았다.

 

이후 tcpdump 기반 접근, Python 파싱, RAW socket 방식까지 단계적으로 시도했고,

최종적으로는 Python에서 AF_PACKET RAW socket으로 직접 패킷을 읽어 UDP payload를 복원하는 방식으로 해결했다.


1. 작업 환경

  • 수집 서버: Linux 서버 (Splunk POC 서버)
  • 인터페이스: eno2
  • 트래픽 입력 방식: TAP 방식 미러링
  • 대상 프로토콜: UDP 514 (syslog)
  • 저장 경로: /data/syslog/<src_ip>/YYYY-MM-DD-HH.log
  • 목표: 패킷에 포함된 syslog payload를 원형에 가깝게 복원하여 파일로 저장

핵심은 일반적인 “장비가 내 서버의 UDP 514로 직접 syslog를 보내는 상황”이 아니라, TAP으로 복제된 패킷을 관찰만 할 수 있는 상황이라는 점이었다.


2. 처음 기대했던 구조와 실제 문제

2.1 기대했던 구조

TAP → NIC → syslog-ng(UDP 514 listen) → 파일 저장

 

처음에는 syslog-ng가 UDP 514를 리슨하고 있으니, 미러링된 syslog도 그대로 받을 수 있을 것이라고 생각했다.

 

2.2 실제 결과

tcpdump로는 패킷이 보였지만, syslog-ng는 반응하지 않았다.

 

2.3 판단 근거

다음과 같은 패킷은 분명히 들어오고 있었다.

11:29:41.717298 IP 192.168.3.164.55827 > 192.168.3.157.514: SYSLOG local4.info, length: 247
11:29:41.717426 IP 165.168.3.164.55827 > 192.168.3.157.514: SYSLOG local4.info, length: 454
11:29:41.775743 IP 172.17.30.36.48749 > 192.168.3.157.514: SYSLOG local0.info, length: 762

 

즉, 패킷 레벨에서는 데이터가 들어오고 있다. 그런데 syslog-ng는 반응하지 않았다. 여기서 내린 판단은 다음과 같았다.

  • 이 트래픽은 “내 서버 소켓으로 정상적으로 전달된 syslog”가 아니라
  • TAP으로 복제되어 NIC에서 관찰되는 패킷이다.
  • 따라서 pcap 계층에서는 보이지만, 커널 UDP socket listener가 받는 형태는 아닐 수 있다.

즉, 문제는 “패킷이 안 들어오는 것”이 아니라, syslog-ng가 기대하는 입력 경로와 실제 입력 경로가 다르다는 점이었다.


3. 1차 시도: tcpdump로 payload를 바로 파일에 떨구기

3.1 시도한 명령어

tcpdump -i eno2 udp port 514 -A

 

이 명령어는 UDP 514 패킷의 payload를 ASCII 형태로 보여주기 때문에, 여기서 <PRI>로 시작하는 syslog만 추출해서 파일에 저장하면 될 것이라고 생각했다.

 

3.2 확인했던 내용

tcpdump -i eno2 udp port 514 -A | grep "<30>"

 

여기서 <30> 형태의 메시지가 보이면 syslog payload가 포함되어 있다는 뜻이다.

 

3.3 실제 관찰 결과

E....N@.-....".h.....n....h.<30>2026-04-09 10:01:29|INFO|131|.........2..._...(VLAN 130)|165.141.37.252|00:E0:4C:68:00:5C|pc1121|.........(Yoon Hyo Jin)|Users|............... ...... ...... .......... NI_NAME=Wi-Fi, MAC=AA:10:E2:1E:E8:CD -> 12:43:F3:4C:8C:BA|NONE

 

3.4 실패 이유

이 방식은 영문 위주의 로그는 어느 정도 보였지만, 한글이 깨졌다.

이유는 단순했다. tcpdump -A는 payload를 “문자열처럼 보이게” 출력해줄 뿐이지, 원본 바이트를 안전하게 보존해주는 방식이 아니었다.

 

특히 한글처럼 멀티바이트 문자는 ASCII 출력 과정에서 이미 손상되었다.

즉, 이 시점에서 내린 판단은 다음과 같았다.

  • tcpdump -A는 분석용 확인에는 쓸 수 있다.
  • 하지만 정식 수집 파이프라인의 입력으로 쓰기에는 부적합하다.

4. 2차 시도: tcpdump 출력 + awk / grep / sed 기반 파싱

4.1 목표

패킷의 source IP를 기준으로 디렉토리를 나누고, syslog payload만 저장하고 싶었다.

목표 경로는 아래처럼 잡았다.

/data/syslog/<src_ip>/YYYY-MM-DD-HH.log

4.2 기본 아이디어

  1. IP 172.17.30.37.37159 > 165.141.3.157.514 같은 헤더 라인에서 src_ip를 추출한다.
  2. 그 다음 줄에서 <134>, <30> 같은 syslog 시작 지점을 찾아 저장한다.

4.3 실패 이유

실제 tcpdump 출력은 내가 기대한 것처럼 “한 줄 = 한 패킷 payload”가 아니었다. 구조는 오히려 아래에 가까웠다.

IP ...
E....binary....
<134>1 2026-04-09T09:16:49+09:00 ...

 

즉, 아래와 같은 가정이 모두 틀렸다.

  • <134>가 항상 줄 맨 앞에 있을 것이다.
  • src IP를 읽은 직후 다음 줄이 바로 payload일 것이다.
  • 문자열 라인 단위로 자르면 패킷 단위로 안정적으로 분리될 것이다.

여기서 발생했던 증상이 다음과 같았다.

  • unknown 폴더 생성: src_ip보다 payload가 먼저 처리되어 디렉토리명이 unknown으로 떨어짐
  • IP 오염: 여러 패킷이 섞이며 마지막 src_ip로 계속 덮어쓰기 됨
  • 164에 몰림: 실제로는 다양한 IP가 들어오는데 하나의 IP로만 저장되는 현상 발생

 

4.4 이 단계에서 내린 판단

문자열 기반 파싱은 결국 tcpdump 출력 포맷에 너무 의존적이었다.

즉, 문제는 정규식이 아니라 입력 자체를 잘못 선택한 것에 가까웠다.


5. 3차 시도: Python + tcpdump subprocess

5.1 접근 이유

awk / sed보다 Python이 버퍼 제어와 디코딩 처리에 유리하다고 판단했다.

5.2 시도한 구조

tcpdump subprocess 실행
→ stdout.readline()
→ src_ip 추출
→ <PRI> 탐지
→ 파일 저장

5.3 사용한 명령

tcpdump -i eno2 -nn -s 0 -l -A udp port 514

5.4 실패 이유

이 구조도 결국 tcpdump의 텍스트 출력 형식에 종속되었다. 특히 아래 두 가지가 치명적이었다.

  • readline() 기준으로는 payload가 한 줄에 온전히 들어오지 않았다.
  • -A가 이미 한글을 손상시키고 있었기 때문에 Python decode를 잘 해도 복원이 불가능했다.

즉, 여기서 중요한 결론이 나왔다.

깨진 ASCII는 나중에 iconv나 decode로 복구할 수 없다.


6. 결정적 확인: 원본 바이트를 봐야 한다

6.1 확인한 명령어

tcpdump -i eno2 udp port 514 -X -s 0

6.2 왜 이 명령을 썼는가

-A는 사람이 읽기 쉬운 대신 원본 바이트를 손상시킬 수 있다.

반면 -XHEX와 ASCII를 같이 보여주기 때문에 원본 바이트를 확인할 수 있다.

 

6.3 관찰한 내용

0x0070:  4345 5054 2cea b888 ed98 b8ed 8380 ec9d
0x0080:  b4ec 96b4 20ec a084 ec82 ac20 eca0 95ec

 

이걸 보고 확인한 사실은 다음과 같았다.

  • 한글은 깨진 것이 아니라 원본 바이트로는 정상적으로 존재하고 있다.
  • 문제는 네트워크가 아니라 내가 tcpdump 출력 문자열을 입력으로 사용한 방식에 있었다.

7. 방향 전환: 아예 RAW packet을 직접 읽자

7.1 판단

이 시점에서 구조를 완전히 바꿨다.

이전 방식:

TAP → tcpdump → 문자열 파싱 → 파일

 

변경 방식:

TAP → Python RAW socket(AF_PACKET) → Ethernet/IP/UDP 직접 파싱 → 파일

7.2 왜 이 방식이 유리한가

  • tcpdump 출력 포맷에 의존하지 않는다.
  • 원본 bytes를 그대로 다룰 수 있다.
  • IP 헤더, UDP 헤더, payload를 직접 파싱할 수 있다.
  • 한글 인코딩도 bytes 기준으로 정확하게 decode할 수 있다.

8. RAW socket 구현 과정

8.1 첫 번째 문제: Python 버전 호환성

처음에는 타입 힌트를 아래처럼 작성했다.

def decode_payload(payload: bytes) -> str | None:

 

그런데 systemd로 실행하자 바로 죽었다.

8.2 확인 명령어

journalctl -u raw-udp-syslog-capture -n 50

8.3 확인한 에러

TypeError: unsupported operand type(s) for |: 'type' and 'NoneType'

8.4 판단

현재 Python 버전이 3.10 미만이라 str | None 문법을 지원하지 않는다고 판단했다.

8.5 수정 방법

from typing import Optional

def decode_payload(payload: bytes) -> Optional[str]:

 

즉, 이 문제는 로직 문제가 아니라 Python 버전과 타입 힌트 문법 호환성 문제였다.


9. RAW socket 방식에서 사용한 핵심 로직

9.1 전체 흐름

  1. AF_PACKET RAW socket으로 eno2에서 패킷 수신
  2. Ethernet header 파싱
  3. IPv4 packet 여부 확인
  4. UDP 여부 확인
  5. dst port가 514인지 확인
  6. payload에서 <PRI> 위치를 찾아 syslog 시작점으로 사용
  7. UTF-8 / CP949 / EUC-KR 순으로 decode 시도
  8. /data/syslog/<src_ip>/YYYY-MM-DD-HH.log에 저장

9.2 왜 쿼리와 필터를 이렇게 짰는가

“쿼리”라고 할 수 있는 건 결국 어떤 패킷을 살리고 어떤 패킷을 버릴지에 대한 조건이었다.

핵심 필터는 다음과 같았다.

  • EtherType = IPv4: IPv6, ARP 등 불필요한 패킷 제외
  • IP protocol = UDP: TCP 등 제외
  • dst_port = 514: syslog 이외의 UDP 제외
  • payload 내 <\d+> 존재: syslog 형태가 아닌 payload 제외

이렇게 한 이유는 아주 단순하다. TAP 환경에서는 불필요한 패킷도 많이 보이기 때문에, 필터를 최대한 초반에 걸어줘야 성능과 정확도가 같이 확보된다.


10. src_ip가 192.168.3.164로만 보이는 문제 분석

10.1 처음 증상

로그는 잘 쌓이는데 모든 디렉토리가 192.168.3.164로만 생성되는 문제가 있었다.

10.2 처음 의심

  • 실제로 164만 보내는가?
  • TAP 장비가 src를 바꾸는가?
  • 코드가 src_ip를 잘못 파싱하는가?

10.3 확인 명령어

tcpdump -i eno2 udp port 514 -nn -s 0

10.4 관찰 결과

11:29:41.717298 IP 192.168.3.164.55827 > 192.168.3.157.514: SYSLOG local4.info, length: 247
11:29:41.717426 IP 192.168.3.164.55827 > 192.168.3.157.514: SYSLOG local4.info, length: 454
11:29:41.775743 IP 192.168.3.36.48749 > 192.168.3.157.514: SYSLOG local0.info, length: 762

10.5 판단

여기서 확정할 수 있었다.

  • 패킷은 실제로 다양한 src_ip로 들어오고 있다.
  • 그런데 내 코드 로그에는 계속 latest_src_ip=192.168.3.164만 찍히고 있었다.
  • 즉, 이건 네트워크 구조 문제가 아니라 코드가 src_ip와 payload를 잘못 매핑하고 있는 문제였다.

 

10.6 수정 방향

이후에는 payload 저장 시점의 “현재 src_ip”를 단순 전역 변수처럼 쓰지 않고, 패킷 하나를 파싱할 때 해당 패킷의 src_ip와 payload를 같이 처리하도록 바꾸었다.

즉, 아래처럼 구조를 바꿨다.

packet 수신
→ parse_ipv4_udp(packet)
→ src_ip, dst_port, payload를 한 번에 획득
→ 조건 맞으면 같은 packet의 src_ip로 저장

 

이 수정으로 src_ip 오염 문제가 사라졌다.


11. 한글 디코딩 전략

11.1 왜 UTF-8만 쓰지 않았는가

로그 장비마다 인코딩이 반드시 UTF-8이라고 단정할 수 없었다. 실제로 한글 포함 로그는 장비별로 다를 수 있기 때문에 fallback이 필요했다.

11.2 사용한 순서

UTF-8 → CP949 → EUC-KR

11.3 이유

  • UTF-8: 최근 장비와 웹 기반 시스템에서 가장 흔함
  • CP949: 국내 Windows 계열 로그에서 자주 등장
  • EUC-KR: 구형 장비/애플리케이션에서 여전히 가능성 존재

이 순서로 decode를 시도하면 대부분의 한글 로그를 무리 없이 복원할 수 있었다.


12. systemd 등록과 운영화 (서비스화 과정)

12.1 왜 systemd로 등록했는가

초기에는 Python 스크립트를 직접 실행해서 테스트했다.

python3 /opt/syslog/raw_udp_syslog_capture.py

 

하지만 이 방식은 다음과 같은 문제가 있었다.

  • 서버 재부팅 시 자동 실행되지 않음
  • 프로세스가 죽어도 자동으로 재시작되지 않음
  • 운영 환경에서 관리가 어려움

따라서 운영 환경에서 안정적으로 돌리기 위해 systemd 서비스로 등록했다.


12.2 systemd 서비스 파일 작성

서비스 파일 경로는 다음과 같이 생성했다.

vi /etc/systemd/system/raw-udp-syslog-capture.service

 

작성한 내용은 다음과 같다.

[Unit]
Description=Raw UDP Syslog Capture from TAP
After=network.target

[Service]
Type=simple
ExecStart=/usr/bin/python3 /opt/syslog/raw_udp_syslog_capture.py
Restart=always
RestartSec=3
User=root
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

 


12.3 각 항목을 이렇게 설정한 이유

[Unit]

  • After=network.target

네트워크 인터페이스(eno2)가 올라온 이후에 실행되도록 하기 위함이다. RAW socket을 사용하는 구조이기 때문에 네트워크가 준비되지 않은 상태에서 실행되면 실패할 수 있다.

 

[Service]

  • Type=simple

Python 스크립트를 foreground로 실행하는 구조이므로 simple 타입으로 설정했다.

  • ExecStart=/usr/bin/python3 /opt/syslog/raw_udp_syslog_capture.py

실제 수집 스크립트를 실행하는 부분이다. 여기서 python 경로는 반드시 which python3로 확인한 경로를 사용해야 한다.

which python3

 

  • Restart=always

스크립트가 예외로 죽거나 오류가 발생해도 자동으로 재시작되도록 설정했다.

패킷 수집기는 항상 살아 있어야 하기 때문에 필수 옵션이다.


  • RestartSec=3

재시작 간격을 3초로 설정했다. 즉시 재시작이 아니라 약간의 딜레이를 둬서 CPU 스파이크를 방지한다.


  • User=root

RAW socket(AF_PACKET)을 사용하기 때문에 root 권한이 필요하다. 일반 사용자로 실행하면 아래와 같은 에러가 발생한다.

Operation not permitted

 

  • StandardOutput=journal
  • StandardError=journal

stdout/stderr를 journalctl로 확인할 수 있게 하기 위함이다. 초기 디버깅 단계에서는 매우 유용했다.


[Install]

  • WantedBy=multi-user.target

서버 부팅 시 자동으로 서비스가 올라오도록 설정하는 부분이다.


12.4 서비스 적용 및 실행

서비스 파일 작성 후 아래 명령어로 적용했다.

systemctl daemon-reload
systemctl enable raw-udp-syslog-capture
systemctl start raw-udp-syslog-capture

12.5 상태 확인

systemctl status raw-udp-syslog-capture

정상 상태는 다음과 같이 표시된다.

Active: active (running)

12.6 로그 확인

journalctl -u raw-udp-syslog-capture -f

 

또는 별도로 작성한 로그 파일을 확인할 수도 있다.

tail -f /var/log/raw_udp_syslog_capture.log

12.7 운영 중 문제와 조치

초기에는 디버깅을 위해 아래와 같은 로그를 남겼다.

write_count=422000, latest_src_ip=192.168.3.164

 

하지만 운영 환경에서는 불필요하게 로그가 너무 많이 쌓여 디스크와 성능에 영향을 줄 수 있었다.

따라서 최종적으로는:

  • 정상 흐름 로그 제거
  • 에러 로그만 유지

또는 아래와 같이 로그 파일 자체를 버리도록 설정할 수도 있다.

rm -f /var/log/raw_udp_syslog_capture.log
ln -s /dev/null /var/log/raw_udp_syslog_capture.log

12.8 최종 정리

systemd로 등록하면서 얻은 효과는 다음과 같다.

  • 서버 재부팅 후 자동 실행
  • 프로세스 장애 시 자동 복구
  • journalctl 기반 모니터링 가능
  • 운영 환경에서 안정적인 서비스 형태로 전환

단순 스크립트에서 실제 운영 가능한 수집 서비스로 전환된 단계라고 볼 수 있다.


13. 최종 아키텍처

TAP
→ eno2
→ Python AF_PACKET RAW socket
→ Ethernet / IPv4 / UDP parsing
→ dst port 514 필터링
→ payload에서 <PRI> 기준 syslog 추출
→ UTF-8 / CP949 / EUC-KR decode
→ /data/syslog/<src_ip>/YYYY-MM-DD-HH.log 저장

 


14. 최종 결과

  • syslog-ng가 못 받는 TAP 트래픽을 직접 복원할 수 있게 됨
  • source IP 기준으로 디렉토리 분리 가능
  • 한글 로그 디코딩 가능
  • hourly 파일 생성 가능
  • systemd 서비스로 운영 가능

15. 이 작업에서 얻은 핵심 교훈

15.1 미러링 트래픽은 “패킷”이지 “로그”가 아니다

처음에는 “syslog가 들어오니까 syslog-ng가 받겠지”라고 생각했지만, 실제로는 전혀 다른 문제였다. TAP/미러링 환경에서는 애플리케이션이 아니라 pcap/packet 관점에서 접근해야 한다는 점이 중요했다.

15.2 tcpdump는 분석 도구이지 정식 수집 파이프라인 입력으로는 한계가 있다

-A, -X 같은 옵션은 현상 확인에는 매우 유용했지만, 실제 수집 파이프라인을 만들 때는 결국 원본 bytes를 직접 다루는 구조가 더 안정적이었다.

15.3 문자열 파싱보다 packet parsing이 먼저다

src_ip, dst_port, payload를 “문자열에서 정규식으로 대충 뽑는 방식”은 결국 깨진다. 네트워크 레이어를 직접 파싱하면 오히려 구조가 단순해지고 안정성이 올라간다.


16. 마무리

이번 작업은 단순히 “로그를 파일로 떨구는 스크립트 하나 작성”이 아니었다.

실제로는 아래 문제를 하나씩 분해해서 해결한 과정이었다.

  • 왜 syslog-ng가 반응하지 않는가
  • 왜 tcpdump로는 보이는데 수집은 안 되는가
  • 왜 한글이 깨지는가
  • 왜 source IP가 한쪽으로 몰리는가
  • 왜 문자열 파싱이 계속 불안정한가

결국 해법은 “더 복잡한 정규식”이 아니라, 입력 레이어를 올바르게 선택하는 것이었다.

같은 상황을 다시 만난다면, 이제는 처음부터 아래처럼 접근할 것이다.

미러링/TAP 환경
→ syslog-ng부터 붙이지 말고
→ pcap/raw packet 관점에서 먼저 검증
→ 그 뒤 payload 복원 구조 설계

 

비슷한 환경에서 TAP/미러링 기반 로그 수집을 해야 한다면, 시행착오를 줄이는 데 도움이 되길 바란다.

728x90
반응형