2025년 6월 5일 목요일

vrrp 장비 failover 시 sync 패킷 전송 중 panic 발생 오류

 vrrp 구성 (Active-Standby or Active-Active) 시 UTM 장비 failover 시 sync 패킷 전송 중 panic 발생하는 문제가 있었다.

외부 고객사에서 발생한 문제라 내부 재현은 불가능하여 코드 분석 레벨에서 session 동기화 과정에서의 버그 발생 지점을 분석해보았다.

conntrack 세션을 이용한 iptables 최적화 및 관련 버그

 iptables 정책 적용 후에도 기존 세션이 남아 hit count를 업데이트 하고 있는 이슈를 맡게된 계기로

상태를 가진 세션 기반 방화벽처리에 대해 조사해보았다.


장애 대응 개요

1. iptables rule sequence 수정 후에도 수정 전 정책으로 패킷 카운팅 되고 있음을 확인

2. 해당 호스트는 PC ↔ eth0 (UTM) ↔ eth1 (UTM) ↔ 인터넷 의 구조로 PC 에 대한 라우터 역할을 수행하고 있었음

3. reply 패킷의 경우 원래  PREROUTING hook을 안타지만 인터넷 → PC 에 대한 reply 패킷은 PREROUTING → FORWARD → POSTROUTING hook을 타고 처리됨

4. rule 적용은 PREROUTING hook의 mangle table에서 orig 패킷만  적용됨

5. reply 패킷은 conntrack rule 에 의해 기존 conntrack table의 세션 마크를 재사용하도록 되어있음

5. rule 수정 후 reply 패킷이 PREROUTING hook을 탈 경우 수정한 rule 이 반영되지 않고 conntrack table의 세션 마크에 의해 수정전의 룰이 반영됨


재현 절차

다음 그림과 같이 모델링된 production 구성에서 정책 수정시 수정 전 정책으로 패킷 카운팅되는 문제가 발생했다.


먼저 정책 2개를 추가하고 icmp ping을 흘려보았다.

*mangle
-A PREROUTING -m fivetuple --msrc 100.1.1.0/24 -j MARK --set-mark 0x321002
-A PREROUTING -m fivetuple --msrc 100.1.1.0/24 -j MARK --set-mark 0x1e1001

30, 50 룰 icmp traffic

iptables -t mangle -L -vn
Chain PREROUTING (policy ACCEPT 8108 packets, 579K bytes)
 pkts bytes target     prot opt in     out     source               destination
            193 21954 CONNMARK   all  --  *      *       0.0.0.0/0            0.0.0.0/0           connmark match !0x0/0xffffffffffffffff CONNMARK restore
             49 11461 RETURN     all  --  *      *       0.0.0.0/0            0.0.0.0/0           reply
            144 10493 RETURN     all  --  *      *       0.0.0.0/0            0.0.0.0/0           connmark match 0x1/0x1
            165 11026 MARK       all  --  *      *       0.0.0.0/0            0.0.0.0/0           MARK set 0xfde80001
            105  7261 MARK       all  --  *      *       0.0.0.0/0            0.0.0.0/0           --msrc 192.168.1.0/24 MARK set 0x321002
            105  7261 MARK       all  --  *      *       0.0.0.0/0            0.0.0.0/0           --msrc 192.168.1.0/24 MARK set 0x1e1001
            105  7261 MARK       all  --  *      *       0.0.0.0/0            0.0.0.0/0           --msrc 192.168.1.0/24 MARK or 0xa000000000000
              0     0 MARK       icmp --  *      *       0.0.0.0/0            0.0.0.0/0           icmp type 13 MARK set 0x0
            165 11026 CONNMARK   all  --  *      *       0.0.0.0/0            0.0.0.0/0           CONNMARK save mask 0xffffffffffffffff


30 → 20, 50 → 30 룰 수정 후 traffic test (이 경우 우선순위가 낮은 30번 룰이 카운팅 되는 문제였다.)

iptables -t mangle -L -vn
Chain PREROUTING (policy ACCEPT 15377 packets, 1077K bytes)
 pkts bytes target     prot opt in     out     source               destination
            228 16008 CONNMARK   all  --  *      *       0.0.0.0/0            0.0.0.0/0           connmark match !0x0/0xffffffffffffffff CONNMARK restore
             16  1261 RETURN     all  --  *      *       0.0.0.0/0            0.0.0.0/0           reply
            212 14747 RETURN     all  --  *      *       0.0.0.0/0            0.0.0.0/0           connmark match 0x1/0x1
             77  4192 MARK       all  --  *      *       0.0.0.0/0            0.0.0.0/0           MARK set 0xfde80001
              5   323 MARK       all  --  *      *       0.0.0.0/0            0.0.0.0/0           --msrc 192.168.1.0/24 MARK set 0x1e1002
              5   323 MARK       all  --  *      *       0.0.0.0/0            0.0.0.0/0           --msrc 192.168.1.0/24 MARK set 0x141001
              5   323 MARK       all  --  *      *       0.0.0.0/0            0.0.0.0/0           --msrc 192.168.1.0/24 MARK or 0xa000000000000
              0     0 MARK       icmp --  *      *       0.0.0.0/0            0.0.0.0/0           icmp type 13 MARK set 0x0
             77  4192 CONNMARK   all  --  *      *       0.0.0.0/0            0.0.0.0/0           CONNMARK save mask 0xffffffffffffffff


룰을 수정하지 않아도 iptables 의 후순위 룰의 패킷수는 카운팅이 되었다.

mangle tables 에서는 access/drop/reject 에 대한 마킹만하고 실제 정책을 확인하여 패킷에 대한 drop 판단을 하는 곳은 filter tables이다. 그렇기 때문에 iptables의 결과는 문제가 없었다.

UTM 장비에서 packet counting 로직은 filter tables 에서 last time 을 업데이트하면 패킷 카운팅 결과를 출력하는 로직으로 구성되어있었다.


조사내역

1. 룰이 netfilter hook을 타고 이동하면서 정책이 적용되고 카운팅되는 로직 분석

2. 상태를 가진 세션의 경우 룰 변경 시 어떻게 상태를 유지하며 룰이 반영되는지



룰이 netfilter hook을 타고 이동하면서 정책이 적용되고 카운팅되는 로직 분석

iptables 의 경우 순차적으로 매칭하여 룰 개수 증가 시 성능이 급격하게 저하되는데 사내 UTM 장비의 경우 iptables-tng 코드를 패치하여 tuple 기반으로 hash table을 활용한 classify 기능을 추가하여 룰 개수가 많아져도 일정한 CPS를 유지하도록 하도록 개발되어있었다.

관련 개발 사항과 룰 적용 시 패킷이 netfilter hook에서 어떻게 처리되는지 분석해보았다.



상태를 가진 세션의 경우 룰 변경 시 어떻게 상태를 유지하며 룰이 반영되는지



SSH Connection Layer에 사용자 패스워드 만료 검증 로직 추가

 개발 내역

다음과 같이 ssh 접속시 패스워드 변경 기능을 추가한다.


SSH Packet

먼저 SSH의 packet은 다음과 같이 구성되어 있다.

Packet LengthPadding LengthPayloadRandom PaddingMessage
Authentication Code
(MAC)
// ssh packet 형식
uint32   packet_length // payload + padding 길이 (4바이트)
byte     padding_length // padding 길이 (1바이트)
byte[n1] payload // 메시지 데이터 (메시지 타입 + 메시지 본문)
byte[n2] random padding // 암호화 블록 크기 맞추기용 랜덤 패딩
[MAC]    optional, if MAC is enabled // 메시지 인증 코드 (무결성 확인용, 선택적)

// 키 교환 패킷 예시 (SSH_MSG_KEXINIT)
packet_length:    0x000001fc
padding_length:   0x0c
payload:          (message type 20) + kex algorithms, hostkey algorithms, etc.
padding:          12 bytes of random data
MAC:              (only if enabled, e.g. hmac-sha2-256)

Wireshark로 캡처한 SSH 패킷이다.

HMAC은 세 가지 필드로 구성되어있다. 패딩, 페이로드, 랜덤 패딩의 내용은 암호화된 패킷 필드에 있다.




중요한 메시지 타입 (payload의 첫 바이트)

(https://www.iana.org/assignments/ssh-parameters/ssh-parameters.xhtml)

메세지 타입 번호메세지 이름설명
1SSH_MSG_DISCONNECT연결 종료
2SSH_MSG_IGNORE무시 가능한 패킷
20SSH_MSG_KEXINIT키 교환 초기화
21SSH_MSG_NEWKEYS새로운 키 적용
30~49키 교환 관련 메시지들ECDH 등 포함
50SSH_MSG_USERAUTH_REQUEST사용자 인증 요청
52SSH_MSG_USERAUTH_SUCCESS인증 성공
90SSH_MSG_CHANNEL_OPEN채널 열기
94SSH_MSG_CHANNEL_DATA터미널 데이터 송신



SSH Protocol Stack

SSH 는 다음과 같은 세가지 레이어로 구성되어있다. 


  • transport layer: 알고리즘 교섭(negotiation), 키 교환을 담당한다. (RFC 4253)
  • authentication layer: 사용자 인증을 담당한다. password(질문-답변), keyboard-interactive, public-key(공개키 기반) 등의 방법이 있다. (RFC 4252) 
  • connection layer: 실제로 터미널을 열거나 하는 과정을 담당 (RFC 4254)







SSH connection layer


1. SSH 연결이 설정되면, 클라이언트와 서버는 키 교환을 통해 안전한 통신 채널을 설정한다.

2. 클라이언트는 서버에 사용자 인증을 수행한다.

3. 인증이 성공하면 클라이언트는 SSH_MSG_CHANNEL_OPEN 메시지를 보내 새로운 채널을 열 수 있다.

byte      SSH_MSG_CHANNEL_OPEN
string    "session"
uint32    sender channel
uint32    initial window size
uint32    maximum packet size

4. 채널이 생성된 후 클라이언트가 SSH_MSG_CHANNEL_OPEN 메시지를 보내고, 서버가 이 요청을 수락하면, 클라이언트는 이제 해당 채널을 통해 데이터를 주고받을 수 있다.

  • SSH_MSG_CHANNEL_OPEN_CONFIRMATION       91
  • SSH_MSG_CHANNEL_OPEN_FAILURE            92

5. 클라이언트는 SSH_MSG_CHANNEL_REQUEST 메시지를 통해 특정 작업을 요청할 수 있다. (pty-req, shell)




Dropbear 를 이용한 SSH 서버 구성

Dropbear는 SSH 프로토콜의 경량 구현이다. SSH 서버는 Dropbear 기반으로 MatrixPKI(matrixssl에 포함된 버전으로)  또는 OpenSSL 호환 API로 구현하는 호환 레이어(cryptowrapper*)로 구성되어있다.


dropbear 커스텀 코드 분석

  • svr-main.c:
    • 연결을 받는 connection pool을 만드는 과정 → thread (event loop)로 대체
    • 실제로는 ipv4 / ipv6 socket 생성만 하고 끝나는 코드라 삭제 가능
    • 대신 bind(), listen() 등 옵션 세팅만 유지하는 형태로 개발
  • svr-chansession.c
    • shell 실행을 위한 channel session을 관리
    • svr-chantypes 는 session type 별 핸들러를 지정하는 구조. 여기서 &svrchansess 를 바꾸면 handler 를 교체하여 새로운 처리 방식 가능
    • 핵심은 channel request-handler 에서 connection 에 적절한 readfd /writefd를 설정해주고 실제 데이터 read/write 는 common-channel.c 에서 처리됨
    • 실제 channel operation 은 common-channel.c 에서 이루어짐
    • 비밀번호 수정 로직과 연결할 때 pipe 를 만들어 연결하면 dropbear 는 그것을 통해 사용자 입출력을 주고 받을 수 있다.
  • authentication
    • dropbear 는 기본적으로 public key 기반 인증을 지원하지만 기존 password 인증로직에 연결해야함

비밀번호 인증 절차

비밀 번호 인증 절차는 다음과 같은 절차로 이루어진다.

1. 클라이언트 -> 서버 (SSH_MSG_USERAUTH_REQUEST)

  • username, service, method(password, keyboard-interactive)을 서버에 요청

2. 서버 -> 클라이언트 - SSH_MSG_USERAUTH_INFO_REQUEST

3. 클라이언트 -> 서버 - SSH_MSG_USERAUTH_INFO_RESPONSE - password

byte      SSH_MSG_USERAUTH_REQUEST
string    user name
string    service name
string    "password"
boolean   FALSE
string    plaintext password in ISO-10646 UTF-8 encoding [RFC3629]

4. 서버 -> 클라이언트 - SSH_MSG_USERAUTH_SUCCESS


비밀번호 만료 로직 설계

비밀 번호 인증 로직을 기반으로 비밀번호 만료 로직을 다음과 같이 설계했다.


1. 클라이언트 -> 서버 - SSH_MSG_USERAUTH_REQUEST

2. 서버 -> 클라이언트 비밀 번호 요청 - SSH_MSG_USERAUTH_INFO_REQUEST

3. 클라이언트 -> 서버 - SSH_MSG_USERAUTH_INFO_RESPONSE

4. 서버 -> 클라이언트 비밀번호 만료, 새 비밀번호 요청 - SSH_MSG_USERAUTH_INFO_REQUEST

byte      SSH_MSG_USERAUTH_PASSWD_CHANGEREQ
string    prompt in ISO-10646 UTF-8 encoding [RFC3629]
string    language tag [RFC3066]

byte      SSH_MSG_USERAUTH_REQUEST
string    user name
string    service name
string    "password"
boolean   TRUE
string    plaintext old password in ISO-10646 UTF-8 encoding [RFC3629]
string    plaintext new password in ISO-10646 UTF-8 encoding [RFC3629]

5. 클라이언트 -> 서버 새 비밀번호를 두 번 입력 - SSH_MSG_USERAUTH_INFO_RESPONSE

6. 서버 -> 클라이언트 비밀번호 변경 성공 알림 - SSH_MSG_USERAUTH_INFO_REQUEST -> SSH_MSG_USERAUTH_SUCCESS


Dropbear SSH 서버 기반으로 비밀번호 만료 로직 개발

0. _dropbear_log, _dropbear_exit 등 callback 함수 치환

  • Dropbear 내부 로깅 함수와 종료 함수를 **시스템 로그 방식(zlog)**이나 커스터마이징된 종료 방식으로 대체
#define _dropbear_log(...) zlog_info(__VA_ARGS__)
#define _dropbear_exit(...) custom_exit_handler(__VA_ARGS__)


1. svr_session 구조를 통째로 쓰지 않고 필요한 부분만 직접 실행

  • svr_session은 Dropbear에서 세션 설정을 종합적으로 다루는 구조체지만,
  • 비밀번호 만료로직과 연동하는 방식이므로 필요한 부분만 직접 초기화하고 사용.
  • 예: ses.channel, ses.authstate, ses.sock_in, ses.remotehost 등만 수동 설정.

2. chaninitialise() 할 때 우리가 만든 핸들러로 교체

  • Dropbear는 chaninitialise()에서 여러 채널 타입 (session, x11, direct-tcpip)을 등록.
  • 여기서 svrchansess 핸들러를 우리가 만든 vty 기반 핸들러로 바꿔 등록.
svr_chantypes[CHAN_SESSION].inithandler = my_vty_chansess_init;


3. ses.packettypes에 custom handler 등록

  • SSH_MSG_USERAUTH_REQUEST → 사용자가 로그인 시 보내는 메시지
  • 해당 메시지에 대해 custom handler 등록
static const packettype svr_packettypes[] = {
  {SSH_MSG_USERAUTH_REQUEST, recv_msg_userauth_request}, /* server */

4. service_loop() 직접 호출

  • dropbear 는 내부적으로 event loop(service loop) 를 통해 패킷 처리 및 세션 유지
  • 기존 svr_main_loop() 에서 이 함수를 호출하는데 여기서는 직접 호출하여 흐름 제어

5. session 수동 생성

  • pipe 대신 socketpair(AF_UNIX, SOCK_STREAM) 사용
  • 이유: pipe 는 반이중, vty->sock 은 전이중(full-duplex)을 기대 → socketpaire 로 해결
int sv[2];
socketpair(AF_UNIX, SOCK_STREAM, 0, sv);
  • 사용자 세션 정보를 갖는 터미널의 한쪽에 dropbear 의 channel에 연결

6. 인증 처리

  • SSH 접속 → USERAUTH_REQUEST → 비밀번호 변경로직

7. channel 과 세션 연결

  • 세션 생성 시 만든 socketpair의 다른 쪽을 channel의 read/write fd로 설정
  • 이후의 event loop는 thread로 대체
    • Dropbear는 select() 기반 polling 사용 → 이를 thread_add_read() 등으로 변경

vrrp 장비 failover 시 sync 패킷 전송 중 panic 발생 오류

  vrrp 구성 (Active-Standby or Active-Active) 시 UTM 장비 failover 시 sync 패킷 전송 중 panic 발생하는 문제가 있었다. 외부 고객사에서 발생한 문제라 내부 재현은 불가능하여 코드 분석 레벨에서 se...