본문 바로가기

지식/Network

Linux netfilter Hacking HOWTO

차례
1. 서문
1.1. 넷필터(netfilter)란 무엇인가?
1.1.1. 커널 2.0과 2.2에서의 문제점?
1.1.2. 누구시죠?
1.1.3. 그게 왜 폭주하죠?
2. 어디서 최신 버전을 구하죠?
3. 넷필터 아키텍처
3.1. 넷필터의 기초
3.2. 패킷 선택: IP Tables
3.2.1. 패킷 필터링
3.2.2. NAT
3.2.3. 매스커레이딩, 포트 포워딩, 투명한 프락시
3.2.4. 패킷 맹글링(packet mangling)
3.3. 연결 추적
3.4. 그이외 추가된 사항
4. 프로그래머들을 위한 정보
4.1. ip_tables의 이해
4.1.1. ip_tables의 데이터 구조
4.1.2. ip_tables의 사용과 진행
4.2. iptables 확장하기
4.2.1. 커널
4.2.1.1. 새로운 Match 함수
4.2.1.2. 새로운 Targets
4.2.1.3. 새로운 Tables
4.2.2. 사용자공간 도구(Userpace Tool)
4.2.2.1. 새로운 Match 함수
4.2.2.2. 새로운 Targets
4.2.3. `libiptc' 사용하기
4.3. NAT의 이해
4.3.1. 연결 추적
4.4. Connection Tracking/NAT 확장하기
4.4.1. 표준 NAT Targets
4.4.2. 새로운 Protocols
4.4.2.1. 커널 내부
4.4.3. 새로운 NAT Targets
4.4.4. 프로토콜 도우미(protocol helper)
4.4.5. 연결 추적 도우미 모듈(Connection Tracking Helper Modules)
4.4.5.1. 설명
4.4.5.2. 사용가능한 구조체와 함수
4.4.6. conntrack 도우미 모듈의 예제
4.4.7. NAT 도우미 모듈
4.4.7.1. 설명
4.4.7.2. 사용가능한 구조체와 함수
4.4.7.3. NAT 도우미 모듈 예제
4.5. Netfilter의 이해
4.6. 새로운 Netfilter 모듈 작성
4.6.1. Netfilter 훅에 연결하기
4.6.2. 큐된 패킷의 처리
4.6.3. 사용자 공간으로부터 명령어 전달받기
4.7. 사용자 공간에서 패킷 처리
5. 커널 2.0/2.2 패킷 필터 모듈 변환
6. 터널 코드 개발자를 위한 Netfilter 훅
7. 시험도구(Test Suite)
7.1. 테스트를 위한 스크립트 작성
7.2. 변수와 환경
7.3. 유용한 도구들
7.3.1. gen_ip
7.3.2. rcv_ip
7.3.3. get_err
7.3.4. local_ip
7.4. 생각나는 대로 하는 충고
8. 개발 동기
9. 감사의 말

1. 서문

본 문서는 여행과도 같으며, 일부분은 아주 쉽게 여행할 수 있고 또 다른 부분에서는 독자 여러분 스스로 길을 찾아야 할 것이다. 필자가 독자에게 할 수 있는 최상의 충고는 아주 큰 머그잔에 커피나 핫쵸콜릿을 가득 담아 편안한 의자에 앉아 위험스런 길을 가기 전에 본문의 내용을 넷트웍 해킹이라는 아주 위험스러운 세상에 부합시켜 깊이 생각해 보라는 것 밖에 없다.

넷필터 프레임웍의 최상위에 있는 내부구조의 사용법을 보다 잘 이해하기 위해서는, Packet Filtering HOWTO와 NAT HOWTO를 읽어보는 것이 좋을 것이다. 커널 프로그래밍에 대한 정보를 얻고자 한다면 Rusty's Unreliable Guide to Kernel Hacking과 Rusty's Unreliable Guide to Kernel Locking을 참고하기 바란다.

(C) 2000 Paul `Rusty' Russell. Licensed under the GNU GPL.


1.1. 넷필터(netfilter)란 무엇인가?

넷필터는 표준 Berkeley socket interface의 외부에 존재하는 packet mangling(패킷을 토막내는 일)에 대한 프레임웍으로, 크게 네 부분으로 구성되어 있다. 먼저 각각의 프로토콜은 "hooks"라는 것을 정의하며, 이는 패킷 프로토콜 스택의 packet's traversal에 있는 잘 정의된 포인터를 의미한다. 이러한 포인터에서, 각각의 프로토콜은 패킷과 훅넘버(hook number)를 이용하여 넷필터 프레임웍을 호출하게 된다.

두 번째로, 커널의 일부분은 각 프로토콜에 대하여 다른 hook을 감시하도록 등록할 수 있다. 따라서 패킷이 넷필터 프레임웍을 통과할 때, 누가 그 프로토콜과 훅을 등록했는지 확인하게 된다. 이러한 것이 등록되어 있다면, 등록된 순서대로 패킷을 검사하고, 패킷을 무시하거나(NF_DROP), 통과시키고(NF_ACCEPT), 또는 패킷에 대한 것을 잊어버리도록 넷필터에게 지시하거나(NF_STOLEN), 사용자 공간에 패킷을 대기시키도록(queuing) 넷필터에게 요청한다(NF_QUEUE).

세 번째 부분은 대기된 패킷을 사용자 공간으로 보내기 위해 제어하는 것으로 이러한 패킷은 비동기방식으로 처리된다.

마지막 부분은 코드와 문서에 기록된 주석문으로 구성되어 있으며, 이는 어떠한 실험적 프로젝트에 대해서도 도움이 되는 부분이다. 넷 필터의 모토는 다음과 같다.

				``그래서... KDE보다 얼만큼 좋다는 거죠?''
			

이러한 저수준 프레임웍과 더불어, 다양한 모듈이 작성되었으며, 이는 이전 버전의 커널에 대하여 유사한 기능, 확장 가능한 NAT시스템 그리고 확장 가능한 패킷 필터링 시스템을 제공한다.


1.1.1. 커널 2.0과 2.2에서의 문제점?

  1. 사용자 공간을 통과하는 패킷에 대하여 어떠한 하부구조도 만들어져 있지 않으며 그 이유는 다음과 같다.

    • 커널수준에서 코딩하는 것이 어렵다

    • 반드시 C/C++로 커널 수준의 코딩이 되어야 한다.

    • 동적 필터링 정책이 커널에 포함되어 있지 않다.

    • 커널 2.2에서는 넷링크를 거처 사용자 공간으로 패킷을 복사하는 방법을 제시하였으나, 재전송 패킷이 느리고 `sanity' check에 지배된다. 예를 들면, 재전송 패킷이 기존의 인터페이스로 들어오도록 요청하는 것이 불가능하다.

  2. 투명한 프락시 구현이 어렵다.

    • 그 주소와 연관된 소켓이 존재하는 지 알아보기 위해 모든 패킷을 살펴본다.

    • Root는 외부 주소와 연결되는 것을 허가해야 한다.

    • 로컬에서 생성된 패킷을 리다이렉트할 수 없다.

    • REDIRECT는 UDP에 대한 응답을 처리하지 못 한다. 예를 들면, UDP 패킷을 1153으로의 리다이렉트가 동작을 하지 않는 경우도 있으며, 이는 일부 클라이언트가 53번 포트가 아닌 다른 곳으로부터 들어오는 응답을 싫어하기 때문이다.

    • REDIRECT는 tcp/upd 포트 재배치에 대하여 관여하지 않는다. 즉, 사용자가 REDIRECT 룰에 의해 결정된 포트를 얻어와야 한다.

    • 커널 2.1 시리즈 개발기간 중 최소 두 번 정도 중단되었다.

    • 코드가 상당히 지저분하다. 커널 2.2.1에서 #ifdef CONFIG_IP_TRANSPARENT_PROXY의 사용 통계를 보면, 11개의 파일에 34번 나타난다. CONFIG_IP_FIREWALL과 비교해 보면, 5개의 파일에 10번만 나타난다.

  3. 인터페이스 어드레스와 별개로 패킷필터 룰을 만드는 것이 불가능하다.

    • 인터페이스를 거쳐오는 패킷이 로컬에서 생성된 것인지 로컬을 들어오는 것인지 구분하기 위해서 반드시 로컬 인터페이스의 주소를 알아야 한다.

    • 리다이렉션이나 매스커레이딩의 경우는 정보가 충분하지 않다..

    • 포워딩 체인만이 외부로 향하는 인터페이스에 대한 정보를 가지고 있다. 네트웍 구성에 대한 지식을 이용하여 어느 인터페이스로부터 패킷이 들어오는지 알고 있어야만 한다.

  4. 매스커레이딩이 필터링에 포함되어 있다:

    필터링과 매스커레이딩간의 상호작용이 방화벽 구축을 복잡하게 만든다:

    • 입력 필터링인 경우, 응답 패킷이 박스 자체를 향하는 것으로 나타난다.

    • 디매스커레이드(demasqueraded)되는 패킷이 포워드 필터링에서는 전혀 나타나지 않는다.

    • 출력 필터링에서는 패킷이 로컬 박스로부터 들어오는 것처럼 보인다.

  5. 포트 포워딩, 라우팅과 QoS에 영향을 줄 수 있는 TOS 처리, 리다이렉트, ICMP 도달불가(unreachable)과 마크(mark) 등이 패킷 필터링 코드에 포함되어 있다.

  6. ipchains 코드는 모듈화되어 있지도 않고 확장할 수도 없다. (예: MAC 어드레스 필터링, 옵션 필터링 등)

  7. 하부구조가 불충분하기 때문에 다른 기술을 낭비하게 만들었다.

    • per-protocol 모듈을 더한 매스커레이딩

    • 라우팅 코드에 의한 빠른 정적 NAT(per-protocol 처리를 포함하지 않았다).

    • 포트 포워딩, 리다이렉트, 자동 포워딩

    • The Linux NAT and Virtual Server Projects.

  8. CONFIG_NET_FASTROUTE와 패킷 필터링간의 호환성 결여:

    • 포워드 패킷은 세 개의 체인을 거쳐 전달된다.

    • 이러한 체인을 무시하고 지나간 경우 알려줄 방법이 없다.

  9. 라우팅 프로텍스로 인해 버려진 패킷을 관찰할 수 없다. (즉, Source Address Verification)

  10. 패킷 필터링 룰에 대하여 자동으로 카운터를 읽어낼 방법이 없다.

  11. CONFIG_IP_ALWAYS_DEFRAG은 컴파일할 때 주는 옵션이라서 일반적인 목적으로 원하는 커널을 배포판을 만들기가 어렵다.


2. 어디서 최신 버전을 구하죠?

최신의 HOWTO, userspace tools 그리고 testsuite를 가지고 있는 CVS 서버가 samba.org에 있다. 통상적인 브라우징 방법으로는, 웹 인터페이스를 사용할 수 있다. 최신의 소스를 얻으려면 다음과 같이 하면 된다:

  1. anoymous로 SAMBA CVS 서버에 로그인한다:

    cvs -d :pserver:cvs@cvs.samba.org:/cvsroot login
    					

  2. 패스워드를 물어보면 `cvs'라고 친다.

  3. 다음 명령을 이용하여 코드를 체크한다:

    cvs -d :pserver:cvs@cvs.samba.org:/cvsroot co netfilter
    					

  4. 최신 버전으로 업데이트하려면, 다음과 같이 한다.

    					
    cvs update -d -P
    					


3. 넷필터 아키텍처

넷필터는 단지 프로토콜 스택의 다양한 포인트에 존재하는 훅의 연속일 뿐이다. 이상적인 IPv4의 진행경로 다이어그램은 다음과 같다.

A Packet Traversing the Netfilter System:
   --->[1]--->[ROUTE]--->[3]--->[4]--->
                 |            ^
                 |            |
                 |         [ROUTE]
                 v            |
                [2]          [5]
                 |            ^
                 |            |
                 v            |
패킷은 그림의 좌측으로부터 들어와서 단순한 데이터 체크(즉, 데이터가 잘렸는지, 혹은 IP 체크 섬의 이상유무, 뒤죽박죽 되지는 않았는 지 등)를 거쳐, 넷필터 프레임웍의 NF_IP_PRE_ROUTING[1] 훅으로 전달된다.

다음으로, 패킷은 라우팅 코드로 들어가며, 여기서 패킷이 다른 인터페이스로 향하는지 또는 로컬 프로세스로 향하는지 결정된다. 패킷이 라우팅될 수 없는 경우, 라우팅 코드는 패킷을 버리기도 한다.

만일 패킷의 목적지가 들어온 박스라면, 넷필터 프레임웍은 패킷을 프로세스로 전달하기 전에 NF_IP_LOCAL_IN [2] 훅을 다시 한번 호출하게 된다.

만일 다른 인터페이스로 전달하고자 한다면, 넷필터 프레임웍은 NF_IP_FORWARD [3] 훅을 호출한다.

그리고 나서 패킷이 넷트웍 라인으로 보내지기 전에 마지막 넷필터 훅인 NF_IP_POST_ROUTING [4] 훅으로 전달된다.

로컬에서 생성된 패킷에 대해서는 NF_IP_LOCAL_OUT [5] 훅이 호출된다. 이 때, 이 훅이 호출된 후 라우팅이 발생하는 것을 알 수 있다. 실제로는 라우팅 코드가 소스 IP 주소와 몇 가지 IP 옵션을 확인하기 위해 라우팅 코드가 먼저 호출이 된다. 즉, 라우팅을 변경하고자 한다면, NAT 코드에 되어 있는 것처럼 여러분 스스로 `skb->dst' 필드를 변경해야만 한다.


3.2. 패킷 선택: IP Tables

IP table을 호출하는 패킷 선택 시스템은 넷필터 프레임웍을 기반으로 구성되었으며, 이는 확장성을 가지고 ipchains로부터 직접 물려받은 유산이다.(ipchains는 ipfwadm으로부터 물려받고, ipfwadm은 BSD의 ipfw IIRC로부터 물려받았다.) 커널 모듈은 새로운 테이블을 등록할 수 있으며, 임의의 패킷이 주어진 테이블을 통과하도록 요청할 수 있다. 이러한 패킷 선택 방법은 패킷 필터링(즉 `필터' 테이블), 네트웍 주소 변환(`nat' 테이블) 그리고 종합적인 pre-route 패킷 mangling(`mangling' 테이블')에 사용한다.

넷필터에 등록된 훅은 다음과 같다.(여기서는 각각의 함수가 실제로 호출되는 순서로 각각의 훅에 있는 함수와 같이 보였다.)

   --->PRE------>[ROUTE]--->FWD---------->POST------>
       Conntrack    |       Filter   ^    NAT (Src)
       Mangle       |                |    Conntrack
       NAT (Dst)    |             [ROUTE]
       (QDisc)      v                |
                    IN Filter       OUT Conntrack
                    |  Conntrack     ^  Mangle
                    |                |  NAT (Dst)
                    v                |  Filter
		


4. 프로그래머들을 위한 정보

비밀을 하나 말씀드리겠습니다. 뭐냐하면, 제가 기르는 햄스터가 모든 코드를 작성했습니다. 저는 단지 전달하는 역할만 했고, 모든 계획은 제 애완동물이 했습죠. 그러니 버그가 생기더라도 저를 원망하지 마시고, 귀여운 털북숭이를 원망하시기 바랍니다.


4.1. ip_tables의 이해

iptables는 메모리 내에 있는 규칙의 명명된 배열과 각각의 훅으로부터 패킷이 전달되기 시작해야 하는 정보를 단순히 제공만 하는 것이다. 어떤 테이블이 등록되고 나면, 사용자 공간은 getsockopt()과 setsockopt()를 이용하여 그 내용을 읽고 변경할 수 있다.

iptables는 어떠한 넷필터 훅에도 등록하지 않으며, 이를 수행하는 다른 모듈에 의존하고 적절히 패킷을 모듈에 전달한다. 다시 말해, 하나의 모듈은 넷필터 훅과 ip_tables에 따로따로 등록해야한 하고, 훅이 발생하면 ip_tables를 호출하는 메커니즘을 제공한다.


4.1.1. ip_tables의 데이터 구조

편리성을 위해, 동일한 데이터 구조를 사용하여 사용자 공간에 의한 규칙과 커널내부의 규칙을 표현하였다. 이렇게 표현된 데이터 구조 중 아주 일부분만이 커널 내부에서 사용된다.

각각의 규칙은 다음과 같은 부분으로 구성된다.

  1. `struct ipt_entry'

  2. zero 또는 그 이상의 `struct ipt_entry_match' 구조로, 각각은 여기에 추가 가능한 데이터의 크기를 변경할 수 있다.

  3. `struct ipt_entry_target' 구조: 추가 가능한 데이터 크기 변화 가능

규칙의 변화 가능한 특성은 확장성에 대하여 상당한 유연성을 제공하며, 특히 각각의 match 혹은 타깃이 임의 크기의 데이터를 전달할 수 있도록 한다. 반면 이는 몇 가지 함정을 만들게 되는 데, 반드시 정렬(alignment)에 주의해야만 한다. `ip_entry'와 `ipt_entry_match', `ipt_entry_target' 구조가 크기 변경이 편리하도록 하고, IPT_ALIGN() 매크로를 이용하여 장비의 최대 정렬(alignment)까지 모든 데이터들을 모으는 것 등을 확실하게 함으로써 정렬을 구현하였다.

`struct ipt_entry'는 다음과 같은 필드를 포함한다.

  1. `struct ipt_ip' : IP header에 대한 세부항목을 포함

  2. `nf_cache' : 현재의 규칙을 검사해야하는 패킷의 부분을 알려주는 비트 필드

  3. `target_offset' : ipt_entry_target 구조가 시작하는 현재 규칙의 시작점으로부터의 offset을 알려주는 필드

  4. `next_offset' : 현재 규칙의 최대 크기를 알려주는 필드로 match와 target을 포함한다. 이 것 역시 IPT_ALIGN 매크로를 이용하여 정렬되어야 한다.

  5. `comefrom' : 패킷의 경로를 추적하기 위해 커널이 사용하는 필드

  6. `struct ipt_counters' : 현재 규칙에 일치하는 패킷에 대한 바이트 카운터와 패킷을 포함하는 필드

`struct ipt_entry_match'와 `struct ipt_entry_target'은 상당히 유사하며, 전체(IPT_ALIGN으로 정렬된) 길이 필드(각각 `match_size'와 `target_size')와, match와 target(사용자 공간에 대한) 명칭을 포함하는 구조체, 그리고 (커널에 대한) 포인터를 포함한다.


4.2. iptables 확장하기

전 무지하게 게으른 넘이라서, iptables는 얼마든지 확장 가능합니다. 다시 말하면 제 손을 떠나 다른 사람에게 넘어간, 오픈소스 그 이상이라는 거죠.

iptables를 확장한다는 것은 다음의 두 부분을 포함한다. 즉, 새로운 모듈을 작성하여 커널을 확장하는 것과 새로운 공유 라이브러리를 작성하여 사용자 차원의 프로그램인 iptables를 확장하는 것이다.


4.2.1. 커널

예제를 보신 사람들은 알겠지만, 커널 모듈을 작성한다는 것 자체는 상당히 단순하다. 한가지 알아야 할 것은 여러분의 코드가 재진입 가능해야 한다는 것이다. 예를 들어보면, 사용자 공간으로부터 들어오는 어떤 패킷이 존재할 수 있을 것이며, 동시에 또 다른 패킷이 인터럽트에 의해 들어 올 수도 있을 것이다. 하지만, 커널 2.3.4이상에서 SMP를 사용할 경우, CPU당 하나의 인터럽트에 대해 하나의 패킷만 존재하게 된다.

여러분들이 알아야 하는 함수는 다음과 같다.

init_module()

모듈의 진입 포인트로 에러가 발생한 경우 음수를 넘겨주며 넷필터에 성공적으로 등록이 된 경우 0을 돌려준다.

cleanup_module()

모듈의 종료 포인트로 넷필터에서 모듈자체를 등록해제한다.

ipt_register_match()

새로운 match 타입을 등록하기 위하여 사용하며, 이 것을 `struct ipt_match'로 전달해야 한다. 통상 `struct ipt_match'는 정적 변수로 정의한다.

ipt_register_target()

새로운 타입을 등록하기 위해 사용하며, 이 것을 `struct ipt_target'으로 전달해야 한다. 보통 `struct ipt_target'은 정적 변수로 전달된다.

ipt_unregister_target()

target을 등록해제하기 위해 사용한다.

ipt_unregister_match()

match를 등록해제하기 위해 사용한다.

여러분이 작성한 새로운 match나 target에 대한 새로운 공간 내에서의 편법 사용(카운터 기능 제공 같은)에 대한 한가지 경고를 하겠다. SMP 머신의 경우 각각의 CPU에 대하여 전체 테이블을 memcpy()를 이용하여 복사한다. 즉 중심이 되는 정보를 보존하기 바란다면, `limit' match에 사용된 방법을 찾아봐야만 할 것이다.


4.2.1.1. 새로운 Match 함수

새로운 match function은 일반적으로 독립모듈로 작성한다. 바꾸어 말하면, 비록 통상적으로 필요하지 않더라도, 이러한 모듈에 확장성을 제공하는 것이 가능하다는 것이다. 따라서, 사용자들이 직접 여러분이 작성한 모듈과 통신할 수 있도록 넷필터 프레임웍의 `nf_register_sockopt' 함수를 사용하는 것이 하나의 방법이 될 것이다. 또 다른 방법은 넷필터 모듈과 ip_tables에 구현된 것과 동일한 방법으로, 다른 모듈이 자신을 등록하도록 심벌을 export하는 것이다.

여러분 작성한 새로운 함수의 핵심은 ipt_register_match()로 전달되는 ipt_match 구조체이다. 이 구조체는 다음과 같은 필드를 포함한다.

list

임의의 값으로 설정되는 필드이다. 즉 `{ NULL, NULL }'

name

사용자 공간에서 참조되는 match함수의 이름을 저장하는 필드이다. 자동 로딩 기능이 동작하기 위해서 함수의 이름은 모듈의 이름과 일치하여야 한다. 예를 들면, 함수이름이 ``mac''인 경우, 모듈이름은 반드시 ``ipt_mac.o''이어야 한다.

match

match 함수의 포인터를 저장하는 필드로, skb, 입/출력 장치의 포인터(훅에 따라 둘 중 하나는 NULL이 될 수도 있다), 동작시킬 룰에 해당하는 match 데이터의 포인터, IP 오프셋(non-zero는 non-head 프래그먼트를 의미), 프로토콜 헤더에 대한 포인터(과거의 IP header), 데이터의 길이(패킷 길이에서 IP 헤더의 길이를 뺀 크기) 그리고 `hotdrop' 변수에 대한 포인터를 취하게 된다. 패킷이 일치하면 non-zero를 돌려야 주어야 하고, 0을 돌려주는 경우 `hotdrop'은 1로 설정할 수 있으며 이는 패킷을 바로 버렸다는 것을 알리기 위한 것이다.

checkentry

룰에 대한 세부명세를 확인하는 함수의 포인터를 저장한다. 등록된 함수가 0을 돌려주는 경우, 현재의 룰은 사용자로부터 받아들여지지 않을 것이다. 예를 들면, ``tcp'' match 타입은 오직 tcp 패킷만 받아들일 것이고, 따라서 룰의 `struct ipt_ip' 부분에 프로토콜은 반드시 tcp이어야 한다고 명시되어 있지 않는 한 0이 리턴 될 것이다. tablename 인자는 사용자의 match가 어떤 테이블을 사용할 수 있는지를 허가하고, `hook_mask'는 사용자의 룰이 호출될 수 있는 훅에 대한 비트매스크이다. 만일 여러분의 match가 넷필터의 일부 훅에 대하여 아무런 의미가 없다면, 그 match를 이 위치에서 없앨 수 있다.

destroy

현재의 match가 삭제될 경우 호출되는 함수의 포인터를 저장하며, 사용자로 하여금 checkentry에서 동적으로 리소스를 재배치하고 또 이를 없앨 수 있도록 한다.

me

`THIS_MODULE'로 설정되며, 이는 여러분의 모듈에 대한 포인터를 돌려준다. 어떤 타입의 규칙이 생성되거나 소멸되는 경우 usage-count를 증가 혹은 감소시킨다. 어떤 규칙이 이 것을 참조하고 있음에도 불구하고 사용자가 모듈을 제거하고자 하는 경우, 사용자가 모듈을 제거하지 못하도록 한다.


4.2.1.2. 새로운 Targets

여러분의 타깃이 패킷(헤더나 바디)을 변경시킨다면, 패킷이 복제되는 시점에서 패킷을 복사하기 위하여 skb_unshare()를 호출해야만 한다. 그렇지 않으면 skbuff에 복제된 패킷이 있는 어떠한 raw socket이라도 변경된 사항을 알아차리게 된다.

새로운 target은 통상적으로 단독 모듈로 작성된다. `New Match Functions'의 절에서 언급한 바와 동일한 내용이 여기서도 적용된다.

여러분의 새로운 target의 핵심은 ipt_register_target()으로 전달되는 struct ipt_target으로, 이 구조체는 다음과 같은 필드를 갖고 있다.

list

임의의 값으로 설정되는 필드이다. 즉 `{ NULL, NULL }'

name

사용자 공간에서 참조되는 target 함수의 이름을 저장하는 필드이다. 자동 로딩 기능이 동작하기 위해서 함수의 이름은 모듈의 이름과 일치하여야 한다. 예를 들면, 함수이름이 ``REJECT''인 경우, 모듈이름은 반드시 ``ipt_REJECT.o''이어야 한다.

target

target 함수의 포인터를 저장하는 필드로, skbuff, 훅 넘버, 입/출력 장치의 포인터(훅에 따라 둘 중 하나는 NULL이 될 수도 있다), target 데이터의 포인터, 테이블에 있는 룰의 위치를 값으로 갖게 된다. 패킷이 계속 진행해야 한다면 target 함수는 IPT_CONTINUE(-1)을 돌려주고, 그렇지 않은 경우는 NF_DROP, NF_ACCEPT, NF_STOLEN등을 돌려주어 패킷의 운명을 결정하게 된다.

checkentry

룰에 대한 세부명세를 확인하는 함수의 포인터를 저장한다. 등록된 함수가 0을 돌려주는 경우, 현재의 룰은 사용자로부터 받아들여지지 않을 것이다.

destroy

현재의 target이 사용중인 entry가 삭제될 경우 호출되는 함수의 포인터를 저장하며, 사용자로 하여금 checkentry에서 동적으로 리소스를 재배치하고 또 이를 없앨 수 있도록 한다.

me

`THIS_MODULE'로 설정되며, 이는 여러분의 모듈에 대한 포인터를 돌려준다. 어떤 타입의 규칙이 생성되거나 소멸되는 경우 usage-count를 증가 혹은 감소시킨다. 어떤 규칙이 이 것을 참조하고 있음에도 불구하고 사용자가 모듈을 제거하고자 하는 경우, 사용자가 모듈을 제거하지 못하도록 한다.


4.2.2. 사용자공간 도구(Userpace Tool)

이제 여러분들이 직접 커널 모듈을 작성했고, 사용자 공간에서 이에 대한 옵션을 조정하기를 원할 것이다. 필자는 각각 확장된 버전에 대하여 새로이 파생된 버전을 만드는 것보다는 아주 최신의 90년대 기술을 사용한다. 즉 furbies이다. 쐬리... 공유라이브러리를 말하는 것이다.

새로운 테이블을 사용하고자 하는 경우 iptables를 확장할 필요는 없고, `-t' 옵션만 주면 된다.

공유라이브러리는 `_init()'함수를 포함해야하며, 이 함수는 모듈이 로딩 되는 시점에서 자동으로 호출된다. 여러분이 작성한 공유라이브러리가 새로운 match나 새로운 target을 포함하느냐에 따라 _init()함수가 `register_match()'나 `register_target()'함수를 호출한다.

공유라이브러리를 제공해야할 필요도 있으며, 구조체의 일부를 초기화하거나 추가 옵션을 제공하는 데 공유라이브러리를 사용할 수도 있기 때문이다. 필자는 공유라이브러리가 아무것도 안 할지라도 공유라이브러리는 라이브러리가 없을 때 발생하는 문제를 줄일 수 있기 때문에 반드시 공유라이브러리를 사용하기를 주장한다.

`iptables.h' 헤더에 유용한 함수들이 정의되어 있으며, 그중 다음과 같은 것들이 상당히 유용하다.

chech_inverse()

전달인자가 `!'인지 검사하고, 맞으면 `invert' 플랙이 설정되고, 그렇지 않으면, `invert' 플랙을 설정한다. true가 리턴된 경우 예제에서 보인 바와 같이 optiond를 증가시켜야한다.

string_to_number()

스트링을 주어진 범위 내의 숫자로 변환한다. 형식이 잘 못 되었거나 범위를 벗어나면, -1이 리턴된다. `string_to_number'는 `strtol'을 사용한다. 다시 말하면, 선행문자열 ``0x''는 16진수라는 것을 의미하고, ``0''은 8진수라는 것을 의미하게 된다.

exit_error()

에러가 검출된 경우 호출되는 함수이다. 일반적으로 첫 번째 인수는 `PARAMETER_PROBLEM'이며, 사용자가 커맨드 라인을 정확하기 사용하지 않았다는 것을 의미한다.


4.2.2.1. 새로운 Match 함수

여러분들이 작성한 공유라이브러리의 _init() 함수는 `register_match()'에 정적 구조체인 `struct iptables_match'에 대한 포인터를 넘겨준다. 이 구조체는 다음과 같은 필드를 포함한다.

next

match의 링크드 리스트를 만들기 위해 사용되는 포인터로, 초기에는 NULL로 설정된다.

name

match 함수의 이름으로, 라이브러리의 이름과 일치해야한다.(즉, `libipt_tcp.so'에 대해서는 ``tcp''와 같이...)

version

일반적으로 NETFILTER_VERSION 매크로로 설정되며, iptables 바이너리파일이 실수로 엉뚱한 공유라이브러리를 선택하지 않도록 하기 위하여 사용된다.

size

현재 사용하는 match에 대한 match 데이터의 크기로, 정확하게 정렬하기 위해서는 IPT_ALIGN() 매크로를 사용하여야 한다.

userpacesize

일부 match에 대해, 커널은 일부 필드를 내부적으로 변경한다. `limit' target이 좋은 예이다. 이는 단순한 `memcmp()'함수로는 두개의 룰을 비교하기에는 부족하다는 것을 의미한다. 만일 이러한 경우가 발생하면 구조체의 시작점에서 변경되지 않는 모든 필드를 위치시키고, 변경되지 않은 필드의 크기를 여기에 집어넣게 된다. 그러나 일반적인 경우, 이 필드는 `size'필드와 동일한 값을 갖는다.

help

옵션의 사용법을 화면에 출력한다.

init

ipt_entry_match 구조체에 존재하는 별도의 공간(만일 존재한다면)을 초기화하는 데 사용할 수 있고, 어떠한 nfcache 비트도 1로 설정한다. `linux/include/netfilter_ipv4.h'의 내용을 이용하여 표현할 수 없는 어떤 것을 검사하고있다면, 간단히 NFC_UNKNOWN 비트와 OR를 취하면 된다. 이 함수는 `parse()'보다 먼저 호출되어야 한다.

parse

커맨드 라인에 알 수 없는 옵션이 주어진 경우 호출되며, 여러분이 작성한 라이브러리에 주어진 옵션이 존재하는 경우 non-zero를 리턴 한다. `!'이 이미 나타난 경우는 `invert'가 TRUE로 설정된다. `flags' 포인터는 여러분들이 작성한 라이브러리의 배타적 사용을 위한 것이며, 특별히 명시된 옵션의 비트매스크를 저장하기 위하여 사용한다. 여러분들은 ncfcache 필드를 확실히 조정할 수 있도록 해야하며, 필요한 경우에는 `ipt_entry_match' 구조체의 크기를 확장할 수 있어야 한다. 다만 그 크기는 반드시 IPT_ALIGN 매크로를 거쳐서 전달되어야 한다.

final_check

커맨드 라인이 파싱된 후 호출되는 함수이며, 여러분의 라이브러리를 위해 예약된 `flags' 정수를 다루게 된다. 이를 이용하면 어떤 강제적인 옵션이 명시되었는 지 확인할 수 있다. 이러한 경우가 발생하면 `exit_error()'을 호출해야 한다.

print

어떤 룰에 대하여 부가적인 match 정보를 출력하기 위해 chain listing 코드에서 사용하는 함수이다. 사용자가 `-n' 플랙을 명시한 경우 numeric flag이 설정된다.

extra_opts

여러분의 라이브러리가 제공하는 부가 옵션의 null-terminated 리스트이다. 이 옵션은 현재 사용 중인 옵션에 더해져서 getopt_long으로 전달된다. 보다 자세한 것은 man 페이지를 보는 것이 좋을 것이다. getopt_long에 대한 리턴 값은 여러분이 작성한 `parse()' 함수에 대한 첫 번째 인자가 된다.

iptables에 의해 내부적으로 사용하기 위한 이 구조체의 마지막 부분에 부가적인 필드가 존재하지만 그 값을 설정할 필요는 없다.

4.2.3. `libiptc' 사용하기

libiptc는 iptable 제어 라이브러리로 iptable 커널 모듈에서 룰을 나열하고 처리하기 위하여 설계되었다. 이 라이브러리가 현재 사용되고 있는 곳은 iptables 프로그램뿐이지만, 다른 툴을 개발하는 곳에도 쉽게 사용할 수 있다. 이 함수를 사용하기 위해서는 루트 권한이 필요하다.

이 함수가 제공하는 표준 target은 ACCEPT, DROP, QUEUE, RETURN, 그리고 JUMP이다. ACCEPT, DROP, QUEUE는 NF_ACCEPT, NF_DROP과 NF_QUEUE로 번역되고, RETURN은 ip_tables가 처리하는 특별한 IPT_RETURN 값으로, JUMP는 chain name으로부터 table내의 실제 오프셋으로 번역된다.

`iptc_init()' 함수가 호출되면, counter를 포함한 테이블이 읽혀지고, 이 테이블은 `iptc_insert_entry()', `iptc_replace_entry()', `iptc_append_entry()', `iptc_delete_entry()', `iptc_delete_num_entry()', `iptc_flush_entries()', `iptc_zero_entries()', `iptc_create_chain()', `iptc_delete_chain()' 그리고 `iptc_set_policy()' 함수에 의해 처리된다.

`iptc_commit()' 함수가 호출되기 전까지는 테이블의 변화가 기록되지 않는다. 따라서, 라이브러리를 사용하는 두 명의 유저가 동일한 chain을 조작하고자 하기 위해 레이스(race)를 하는 경우가 발생할 수 있으며, 이를 막기 위해서는 locking을 사용해야 하지만, 현재는 구현되어 있지 않다.

하지만, counters에 대해서는 레이스(race)가 발생하지 않으며, 이는 tables의 읽기와 쓰기 중에 발생하는 counters의 증가가 새로운 table에 나타나는 방식을 이용하여 counter가 커널에서 기록되기 때문이다.

여기에는 다음과 같은 다양한 helper 함수가 있다.

iptc_first_chain()

table 내의 첫 번째 chain의 이름을 리턴 한다.

iptc_next_chain()

table 내의 다음 chain의 이름을 리턴하며, NULL은 더 이상 chain이 없다는 것을 의미한다.

iptc_builtin()

주어진 chain name이 builtin chain name이면 TRUE를 리턴 한다.

iptc_first_rule()

주어진 chain name내의 첫 번째 룰의 포인터를 리턴하며, NULL인 경우는 chain이 비었다는 것을 뜻한다.

iptc_next_rule()

chain내의 다음 룰에 대한 포인터를 리턴하며, NULL인 경우는 chain의 끝이라는 것을 알려준다.

iptc_get_target()

주어진 룰의 target을 가져온다. 확장된 target이라면, 그에 해당하는 target의 name을 돌려준다. 다른 chain으로의 jump인 경우는 그 chain의 이름을 돌려준다. 또 결정(DROP 같은)인 경우, 그 name을 돌려준다. accounting-style rule처럼 target이 없는 경우는 null string을 돌려준다.

이 함수가 표준 결정(판정)에 대하여 보다 확장된 해석을 제공하기 때문에, ipt_entry 구조체의 `verdict' 필드의 값을 직접 사용하는 대신에 이 함수를 사용하는 것이다.

iptc_get_policy()

builtin chain의 정책을 가져오고, 그 정책에 대한 적중 통계치를 `counters' 인자에 채운다.

iptc_strerror()

iptc 라이브러리내의 failure code에 대하여 보다 의미 있는 해석을 리턴 한다. 어떤 함수가 에러를 발생한 경우, 항상 errno가 설정되며, 이 값은 iptc_strerror() 함수로 전달되어 에러 메시지로 변환된다.


4.3. NAT의 이해

이제 커널의 NAT(Network Address Translation)까지 오셨군요. 여기서 제공되는 하부구조는 효율보다는 완벽성에 중점을 두고 설계된 것이며, 향후 개조를 통해 효율성이 현저하게 증가될지도 모릅니다. 현재 저는 이 넘이 동작한다는 것만으로도 행복합니다.

NAT는 패킷을 전혀 처리하지 않는 connection tracking과 NAT 코드 자체로 분리되었다. connection tracking 역시 iptables 모듈에서 사용하기 위해 설계되었으며, 따라서 NAT가 관심을 두지 않는 상태(state)에 대해서 미묘한 차이를 보이게 된다.


4.3.1. 연결 추적

연결추적(connection tracking)은 우선 순위가 높은 NF_IP_LOCAL_OUT과 NF_IP_PRE_ROUTING 훅으로 훅킹 되며, 이는 패킷이 시스템으로 진입하기 전 그 패킷을 살펴보기 위함이다.

skb에 있는 nfct 필드는 struct ip_conntrack의 내부에 대한 포인터이며, 배열 infos[] 중 한 곳에 존재한다. 즉, 이 배열내의 어떤 요소를 가리키게 함으로써 skb의 상태를 말할 수 있다. 다시 말해, 이 포인터는 state structure와 그 상태에 대한 skb의 관계 모두를 알려주게 된다.

`nfct' 필드를 추출하는 최선의 방법은 `ip_conntrack_get()'을 호출하는 것으로, `nfct' 필트가 세트되어 있으며 connection 포인터를 돌려주고 그렇지 않은 경우는 NULL을 돌려주며, 현재의 연결에 대한 패킷의 관계를 표현하는 ctinfo을 채운다. `nfct'의 값들은 수치화(enumerate)되어 있으며, 다음과 같은 값을 갖는다.

IP_CT_ESTABLISHED

원래 방향에 대하여 established connection의 일부인 패킷이다.

IP_CT_RELATED

connection에 관련된 패킷으로, 원래의 방향으로 전달되고 있다.

IP_CT_NEW

새로운 connection을 생성하고자 하는 패킷이다.(분명히, 원래 진행방향에 존재한다)

IP_CT_ESTABLISHED + IP_CT_IS_REPLY

established connection에 관련된 패킷으로, 응답방향(reply direction)으로 향하고 있다.

IP_CT_RELATED + IP_CT_IS_REPLY

connection에 관련된 패킷으로, 응답방향(reply direction)으로 향하고 있다.

즉, 응답패킷(reply packet)은 nfct를 검사하여 그 값이 IP_CT_IS_REPLY 보다 크거나 같은 값인 가로 확인할 수 있다.

4.4. Connection Tracking/NAT 확장하기

이 방식은 여러 가지 프로토콜과 서로 다른 맵핑 타입을 조절하기 위하여 설계된 것으로, 맵핑 타입 중 일부는 부하분산(load-balancing)이나 fail-over 맵팽 타입처럼 상당히 구체적인 것도 있다.

내부적으로는, connection tracking은 일치하는 룰이나 binding을 검색하기 전에 패킷을 ``tuple''로 변환시킨다. 여기서 ``tuple''이란 패킷 중 관심의 대상이 되는 부분을 말한다. ``tuple''은 처리 가능한 부분과 그렇지 못한 부분으로 구분되며, 각각 ``src''와 ``dst''라고 한다. 이는 Source NAT의 입장에서 첫 번째 패킷에 대한 관점이다. 동일한 방향에 있어 동일한 패킷 스트림의 각 패킷에 대한 tuple은 모두 동일하다.

예를 들어보면, TCP 패킷의 tuple은 처리 가능한 부분을 포함하는 데 이는 source IP와 source PORT이며, 처리 불가능한 부분은 목적지 IP와 목적지 port이다. 처리 가능한 부분과 그렇지 못한 부분은 반드시 같은 타입이어야 할 필요는 없다. 다시 말하면 ICMP 패킷의 tuple은 source IP와 ICMP port 같은 처리 가능한 부분을 포함하며, 처리 불가능한 부분은 목적지 IP와 ICMP 타입과 코드이다.

각각의 패킷은 inverse를 가지고 있으며, 이는 스트림에 있어서 응답패킷의 tuple이다. 이를테면, icmp id가 12345이고 192.168.1.1에서 1.2.3.4로 가는 ICMP ping 패킷의 inverse는 icmp id 12345이고 1.2.3.4에서 192.168.1.1이 된다.

`struct ip_conntrack_tuple'로 표현되는 tuple은 널리 사용되며, 실제로 패킷이 들어오는 훅과 디바이스를 포함하여 패킷에 대한 완전한 정보를 제공한다.

대부분의 tuple은 `struct ip_conntrack_tuple_hash'에 포함되며, 더블링크드 리스트와 tuple이 포함된 connection에 대한 포인터를 추가한다.

connection은 `struct ip_conntrack'에 의해 표현되며, 이 구조체는 `struct ip_conntrack_tuple_hash'필드를 두개 포함한다. 하나는 원본 패킷에 대한 방향(tuplehash[IP_CT_DIR_ORIGINAL])이며, 다른 하나는 응답방향에 대한 패킷(tuplehash[IP_CT_DIR_REPLY])에 대한 것이다.

아무튼, NAT 코드가 하는 첫 번째 일은 skbuff의 nfct 필드를 참조하여 connection tracking 코드로 tuple을 추출할 수 있는 지 확인하고 이미 존재하는 connection을 찾는 것이다. 이것이 의미하는 바는 현재 connection이 새로이 시도된 것인지 아닌지, 그리고 어떤 방향인지를 알려 주는 것이다. 여기서 후자인 경우, 그러니까 이미 연결이 된 상태라면, 이전에 결정된 처리방법이 적용된다.

새로운 connection이 시작되면, 표준 iptable 진행 메커니즘을 이용하여 tuple에 대한 룰을 `nat' table에서 검색한다. 이 때 룰이 일치하는 경우, 보통의 경우 진행방향과 응답방향 모두에 대하여 처리를 초기화한다. 즉, 기대하고 있는 응답이 변경되어 버렸다는 것을 connection-tracking 코드가 알 수 있게 된다. 그리고 나서 앞서 언급한 바와 같이 처리된다.

만일 적용할 룰이 없는 경우 `null' 바인딩이 생성된다. 이 바인딩이 패킷과 맵핑되지 않지만, 기존의 다른 스트림과 맵핑되지 않도록 주의해야 한다. 어떤 경우는 null 바인딩이 생성될 수 없는 경우도 발생하며 이 경우는 null 바인딩을 기존의 스트림으로 이미 맵핑을 해버렸기 때문이다. 이러한 경우는 정상적인 `null' 바인딩이라 하더라도 per-protocol으로 이를 새로 맵핑해야 할 것이다.


4.4.2. 새로운 Protocols

4.4.2.1. 커널 내부

새로운 프로토콜을 구현한다는 것은 tuple의 처리가능 부분과 그렇지 못한 부분을 결정하는 것이다. tuple에 포함된 모든 것은 스트림을 구별할 수 있는 특성을 가지고 있다. tuple의 처리가능부분은 NAT를 적용할 수 있는 부분으로 TCP인 경우는 소스 포트가 되며, ICMP인 경우는 icmp ID가 된다. 즉, 스트림 구별자로 사용할 수 있다는 말이다. 처리 불가능한 부분은 패킷의 나머지 부분으로 스트림을 구별할 수 있지만, 이것을 마음대로 처리할 수 없다. (예: TCP 목적지 포트, ICMP 타입)

한가지가 결정되었으면, connection tracking 코드를 작성할 수 있고, `ip_conntrack_register_protocol()'로 전달하기 위하여 `ip_conntrack_protocol' 구조체를 다루어야 할 것이다.

`struct ip_conntrack_protocol'의 필드는 다음과 같다.

list

{ NULL, NULL }로 설정한다. 리스트를 확보한다.

proto

프로토콜 번호이며 자세한 것은 `/etc/protocols'를 참조하기 바란다.

name

사용자가 보게 될 프로토콜 명칭이다. `/etc/protocols'에 있는 명칭을 사용하는 것이 제일 좋을 것이다.

pkt_to_tuple

주어진 패킷에 대한 tuple의 프로토콜 상세부분을 채우는 함수이다. `datah'라는 포인터는 IP 헤더의 시작부분을 가리키며, datalen은 패킷을 길이이다. 패킷이 헤더 정보를 저장하기에 충분히 길지 않으면, 0을 돌려준다. datalen은 최소 8바이트이다.

invert_tuple

tuple의 프로토콜 상세부분을 단순히 패킷에 대한 응답으로 변경하는 데 사용한다.

print_tuple

tuple의 프로토콜 상세부분을 출력하는 데 사용하는 함수로, sprintf() 함수를 이용한다. 사용된 버터 캐릭터의 수가 리턴된다. /proc 엔트리에 대한 상태를 출력하기 위해 사용하기도 한다.

print_conntrack

conntrack 구조체의 공개되지 않은 부분을 출력하는 데 사용하는 함수로, 간혹 /proc에 있는 상태를 출력하기 위해 사용하기도 한다.

packet

established connection의 일부를 보고자 할 때 호출하는 함수이다. conntrack 구조체, IP 헤더, 길이 그리고 ctinfo에 대한 포인터를 얻게 된다. 패킷에 대한 판결로 통상 NF_ACCEPT를 돌려주며, connection이 유효하지 않은 패킷에 대해서는 -1을 돌려준다. 원한다면 이 함수 내부에서 connection을 제거할 수도 있지만, 레이스(race)를 방지하기 위해서는 다음과 같은 방법을 사용해야만 한다.

if (del_timer(&ct->timeout))
	ct->timeout.function((unsigned long)ct);
								

new

패킷이 최초로 연결을 맺을 때 호출되는 함수로, ctinfo 인자는 없다. 그 이유는 최초의 패킷은 정의에 의해 ctinfo IP_CT_NEW이기 때문이다. 연결을 맺는데 실패하면 0을 돌려주고, 성공한 경우는 순간적으로 connection timeout을 돌려준다.

코드를 작성하고 새로운 프로토콜에 대한 추적을 테스트 했으면, 이제는 NAT에게 이러한 것을 어떻게 해석할 것인지를 알려주어야 한다. 다시 말하면 새로운 모듈을 작성해야 한다는 것이다. 즉, NAT 코드에 대한 확장 및 `ip_nat_protocol_register()'로 전달하고자 하는 `ip_nat_protocol' 구조체를 사용하는 것이다.

list

{ NULL, NULL }로 설정한다. 리스트를 확보한다.

name

사용자가 보게 될 프로토콜 명칭이다. 사용자 공간에 자동으로 로딩 되기 위해서는 `/etc/protocols'에 있는 명칭을 사용하는 것이 제일 좋다.

protonum

프로토콜 번호이며 자세한 것은 `/etc/protocols'를 참조하기 바란다.

manip_pkt

connection tracking의 pkt_to_tuple 함수의 다른 반쪽으로, ``tuple_to_pkt''라고 생각해도 무방하다. 약간 다르게 고려해야 할 점은 다음과 같다. IP 헤더의 시작위치에 대한 포인터와 전체 패킷의 길이를 얻는다는 점으로, 일부 프로토콜(UDP, TCP)이 IP 헤더를 알아야 할 필요가 있기 때문이다. 패킷 전체가 아닌 tuple(즉, ``src'' 필드)로부터 ip_nat_tuple_manip 필드와 수행하고자 하는 처리에 대한 타입을 받아오게 된다.

in_range

주어진 패킷의 처리 가능한 부분이 주어진 범위에 속해있는 지를 알려주는 함수이다. bit tricky 함수로, tuple에 적용할 처리 타입(manipulation type)을 돌려주며, 범위를 어떻게 해석할 것인 가, 즉 처리하고자 하는 것이 소스 범위인지 목적지 범위인가 하는 가를 알려준다.

기존의 맵핑이 올바른 범위에 속해 있는 지 확인하는 데 사용되며, 또한 전혀 처리할 필요가 없는 지 확인하는 데 사용한다.

unique_tuple

NAT의 핵심이 되는 함수이다. tuple과 범위가 주어지면, tuple의 per-protocol 부분을 이 범위에 속하는 tuple로 변경하고, 이 것을 유일하게(unique) 만들어버린다. 주어진 범위에서 사용하지 않는 tuple을 찾아내지 못한 경우 0을 리턴 한다. 또한 ip_nat_used_tuple()에 필요한 conntrack 구조체의 포인터를 얻어낸다.

통상의 방법은 tuple에 대하여 `ip_nat_used_tuple()'을 확인하면서 false가 리턴 될 때까지 범위에서 tuple의 per-protocol 부분을 반복한다.

null-mapping인 경우는 이미 확인이 된 것으로, 주어진 범위 밖에 있거나, 이미 취해진 경우이다.

IP_NAT_RANGE_PROTO_SPECIFIED가 설정되어 있지 않으면, 사용자가 NAPT가 아니라 NAT를 수행하고 있는 것을 의미한다. 즉, 범위 내에서 무엇인가를 한다는 것이다. 맵핑이 필요하지 않다면, 0을 돌려준다.

print

문자버퍼와 범위가 주어진 경우, 그 범위의 per-protocol 부분을 출력하고, 사용된 버퍼의 길이를 돌려준다. IP_NAT_RANGE_PROTO_SPECIFIED 플랙이 주어진 범위에 대해 설정되어 있지 않으면 호출되지 않는다.


4.4.5. 연결 추적 도우미 모듈(Connection Tracking Helper Modules)


4.4.6. conntrack 도우미 모듈의 예제

#define FOO_PORT	111
static int foo_nat_expected(struct sk_buff **pksb,
			unsigned int hooknum,
			struct ip_conntrack *ct,
			struct ip_nat_info *info,
			struct ip_conntrack *master,
			struct ip_nat_info *masterinfo,
			unsigned int *verdict)
/* called whenever a related packet (as specified in the connection tracking
   module) arrives
   params:	pksb	packet buffer
		hooknum	HOOK the call comes from (POST_ROUTING, PRE_ROUTING)
		ct	information about this (the related) connection
		info	&ct->nat.info
		master	information about the master connection
		masterinfo &master->nat.info
		verdict what to do with the packet if we return 1.
{
        /* Check that this was from foo_expect, not ftp_expect, etc */
	/* Then just change ip/port of the packet to the masqueraded
 	   values (read from master->tuplehash), to map it the same way,
           call ip_nat_setup_info, set *verdict, return 1. */
}	
static int foo_help(struct ip_conntrack *ct,	
		struct ip_nat_info *info,
		enum ip_conntrack_info ctinfo,
		unsigned int hooknum,
		struct sk_buff  **pksb)
/* called for the packet causing related packets 
   params:	ct	information about tracked connection
		info	(STATE: related, new, established, ... )
		hooknum	HOOK the call comes from (POST_ROUTING, PRE_ROUTING)
		pksb	packet buffer
*/
{
	/* extract information about future related packets (you can
	   share information with the connection tracking's foo_help).
	   Exchange address/port with masqueraded values, insert tuple
	   about related packets */
}
static struct ip_nat_expect foo_expect = {
	{ NULL, NULL },
	foo_nat_expected };
static struct ip_nat_helper hlpr;
static int __init(void)
{
	int ret;
	if ((ret = ip_nat_expect_register(&foo_expect)) == 0) {
		memset(&hlpr, 0, sizeof(struct ip_nat_helper));
		hlpr.list = { NULL, NULL };
		hlpr.tuple.dst.protonum = IPPROTO_TCP;
		hlpr.tuple.dst.u.tcp.port = htons(FOO_PORT);
		hlpr.mask.dst.protonum = 0xFFFF;
		hlpr.mask.dst.u.tcp.port = 0xFFFF;
		hlpr.help = foo_help;
		ret = ip_nat_helper_register(hlpr);
		if (ret != 0)
			ip_nat_expect_unregister(&foo_expect);
	}
	return ret;
}
static void __exit(void)
{
	ip_nat_expect_unregister(&foo_expect);
	ip_nat_helper_unregister(&hlpr);
}
				


4.4.7. NAT 도우미 모듈


4.4.7.2. 사용가능한 구조체와 함수

여러분이 작성한 nat helper 모듈의 `init()' 함수는 `struct ip_nat_helper'에 대한 포인터를 인자로 하여 `ip_nat_helper_register()' 함수를 호출한다. 인자가 되는 구조체는 다음과 같은 멤버 변수를 포함한다.

list

netfilter에서 내부적으로 사용하는 list 헤더로 { NULL, NULL }로 초기화시킨다.

tuple

`struct ip_conntrack_tuple'로, 우리가 작성한 NAT helper가 관심을 갖는 패킷을 명시한 것이다.

mask

`struct ip_conntrack_tuple'이며, tuple의 어느 비트가 유효한지를 netfilter에게 알려준다.

help

tuple+mask에 부합하는 각 패킷에 대하여 호출해야하는 함수이다.

name

NAT help로 구별이 되는 유일한 name

이상은 connection tracking helper를 작성하는 방법과 완전히 동일하다. 이제 여러분들이 작성한 모듈은 `ip_nat_register()' 함수를 이용하여 예측되는 임의의 connection의 NAT를 처리할 준비가 되어 있다고 말할 수 있다. 이때, `ip_nat_register()' 함수는 `struct ip_nat_expect'를 인자로 취하게 되며, 그 멤버 변수는 다음과 같다.

list

netfilter에서 내부적으로 사용하는 list 헤더로 { NULL, NULL }로 초기화시킨다.

expect

예견된 connection에 대하여 NAT를 수행하는 함수이다. connection을 처리하면 true를 리턴하고, 다음에 등록된 expect 함수가 호출되어 패킷을 처리할 수 있는 지 검사하게 된다. true가 리턴된 경우, 이 함수는 반드시 판결(verdict)을 알려주어야 한다.


4.4.7.3. NAT 도우미 모듈 예제

#define FOO_PORT	111
static int foo_nat_expected(struct sk_buff **pksb,
			unsigned int hooknum,
			struct ip_conntrack *ct,
			struct ip_nat_info *info,
			struct ip_conntrack *master,
			struct ip_nat_info *masterinfo,
			unsigned int *verdict)
/* called whenever a related packet (as specified in the connection tracking
   module) arrives
   params:	pksb	packet buffer
		hooknum	HOOK the call comes from (POST_ROUTING, PRE_ROUTING)
		ct	information about this (the related) connection
		info	&ct->nat.info
		master	information about the master connection
		masterinfo &master->nat.info
		verdict what to do with the packet if we return 1.
{
        /* Check that this was from foo_expect, not ftp_expect, etc */
	/* Then just change ip/port of the packet to the masqueraded
 	   values (read from master->tuplehash), to map it the same way,
           call ip_nat_setup_info, set *verdict, return 1. */
}	
static int foo_help(struct ip_conntrack *ct,	
		struct ip_nat_info *info,
		enum ip_conntrack_info ctinfo,
		unsigned int hooknum,
		struct sk_buff  **pksb)
/* called for the packet causing related packets 
   params:	ct	information about tracked connection
		info	(STATE: related, new, established, ... )
		hooknum	HOOK the call comes from (POST_ROUTING, PRE_ROUTING)
		pksb	packet buffer
*/
{
	/* extract information about future related packets (you can
	   share information with the connection tracking's foo_help).
	   Exchange address/port with masqueraded values, insert tuple
	   about related packets */
}
static struct ip_nat_expect foo_expect = {
	{ NULL, NULL },
	foo_nat_expected };
static struct ip_nat_helper hlpr;
static int __init(void)
{
	int ret;
	if ((ret = ip_nat_expect_register(&foo_expect)) == 0) {
		memset(&hlpr, 0, sizeof(struct ip_nat_helper));
		hlpr.list = { NULL, NULL };
		hlpr.tuple.dst.protonum = IPPROTO_TCP;
		hlpr.tuple.dst.u.tcp.port = htons(FOO_PORT);
		hlpr.mask.dst.protonum = 0xFFFF;
		hlpr.mask.dst.u.tcp.port = 0xFFFF;
		hlpr.help = foo_help;
		ret = ip_nat_helper_register(hlpr);
		if (ret != 0)
			ip_nat_expect_unregister(&foo_expect);
	}
	return ret;
}
static void __exit(void)
{
	ip_nat_expect_unregister(&foo_expect);
	ip_nat_helper_unregister(&hlpr);
}
					


4.6. 새로운 Netfilter 모듈 작성


4.6.2. 큐된 패킷의 처리

ip_queue에 의해 사용되는 인터페이스로, 주어진 프로토콜에 대하여 대기된 패킷을 처리하기 위해 등록할 수 있다. 패킷을 처리하는 것을 방지할 수 있다는 것만 빼고는 훅을 등록하는 것과 유사한 의미를 가지며, 훅이 `NF_QUEUE'로 응답하는 패킷을 확인만 할 수 있다.

대기된 패킷에 대한 관심을 등록하기 위해 사용하는 두개의 함수는 `nf_register_queue_handle()'과 `nf_unregister_queue_handler()'이다. 여러분이 등록하고자 하는 함수는 `nf_register_queue_handler()' 함수로 전달되는 `void *' 포인터와 함께 호출된다.

프로토콜을 처리하기 위해 등록된 함수가 없는 경우는, NF_QUEUE를 리턴 하는 것은 NF_DROP를 리턴하는 것과 마찬가지가 된다.

대기된 패킷에 대한 관심을 등록했으면, 패킷이 큐잉 되기 시작한다. 이제 큐잉 된 패킷을 가지고 무엇을 하던 그건 여러분들의 맘이지만, 처리가 끝난 경우에는 반드시 `nf_reject()' 함수를 호출해야 한다(단순히 kfree_skb()를 호출해서는 안 된다). skb를 재 도입하는 경우는, 큐잉 된 패킷을 skb, 큐잉 된 패킷에 할당된 `struct nf_info'와 판결(verdict)을 전달한다. 그 이유는, NF_DROP은 패킷을 DROP시킬 것이고, NF_ACCEPT는 훅을 통해 계속 반복되도록 할 것이고, NF_QUEUE는 패킷을 다시 대기 시킬 것이고 NF_REPEAT는 패킷을 대기시킨 훅이 또 다시 검사하도록 만들 것이기 때문이다(이 때, 무한루프에 빠지지 않도록 조심할 것).

`struct nf_info'를 살펴보면, 패킷에 대한 부가적인 정보, 즉 패킷이 존재했던 인터페이스와 훅 같은 것을 얻을 수 있다.


6. 터널 코드 개발자를 위한 Netfilter 훅

Tunnel 드라이버 제작자들은 2.4 커널에 대해서는 다음에 보이는 단순한 두개의 규칙을 따르기 바란다.

  • 패킷을 인식할 수 없도록 하려면 skb->nfct를 릴리즈 해야한다(즉, decapsulating/encapsulating). 만일 패킷을 *new* skb로 감싸지 않으려면 릴리즈를 하지 않아도 되지만, 적절한 곳에서 수행하고자 한다면, 반드시 릴리즈 해야 한다.

    그렇지 않은 경우는, NAT 코드는 패킷을 맹글하기 위해 과거의 connection tracking 정보를 이용할 것이고, 이로 인해 순서가 엉망이 될 것이다.

  • encapsulated 패킷은 반드시 LOCAL_OUT 훅을 통과하도록 해야하며, decapsulated 패킷은 PRE_ROUTING 훅을 통과해야만 한다. 이를 수행하기 위해 대부분의 tunnel은 ip_rcv()를 사용한다.

    그렇지 않은 경우는, 사용자들이 tunnel을 이용하여 원하는 대로 필터링할 수 없을 것이다.

첫 번째를 수행하는 표준 방법은 패킷을 wrap하거나 unwrap하기 전에 다음과 유사한 코드를 삽입하는 것이다.

	/* Tell the netfilter framework that this packet is not the
          same as the one before! */
#ifdef CONFIG_NETFILTER
	nf_conntrack_put(skb->nfct);
	skb->nfct = NULL;
#ifdef CONFIG_NETFILTER_DEBUG
	skb->nf_debug = 0;
#endif
#endif
		

두 번째를 수행하기 위해 필요로 하는 것은 새로이 encapsulated된 패킷이 ``ip_send()''로 들어가는 위치를 찾아내고, 이를 다음과 같은 것으로 대체하는 것이다.

	/* Send "new" packet from local host */
	NF_HOOK(PF_INET, NF_IP_LOCAL_OUT, skb, NULL, rt->u.dst.dev, ip_send);
		

다음에 보인 룰이 의미하는 것은 tunnel 박스에 패킷 필터링 룰을 적용하고자 하는 사람이 tunnel 되고 있는 패킷에 대해 다음과 같은 절차를 보게 될 것이다.

  1. FORWARD hook : normal packet (from eth0 -> tunl0)

  2. LOCAL_OUT hook : encapsulated packet (to eth1)

응답 패킷에 대해서는
  1. LOCAL_IN hook: encapsulated reply packet (from eth1)

  2. FORWARD hook: reply packet (from eth1 -> eth0).


7. 시험도구(Test Suite)

test suite는 CVS 저장소에 있으며, test suite가 적용되는 범위가 넓을 수록, 여러분의 코드가 오동작을 하는 경우가 거의 없을 수 있다는 자신감은 더 커지게 된다. 사소한 테스트(trivial test)는 상당히 어려운 테스트(tricky test) 만큼이나 중요하며, 이는 복잡한 시험을 단순화한 것이 사소한 테스트이기 때문이다. 일단 복잡한 테스트를 수행하기 전에 기본적인 작업은 만족한다는 것을 알 수 있기 때문이다.

테스트는 단순하며, 그저 testsuite/의 서브디렉토리에 있는 쉘스크립트이다. 실행되는 순서는 알파벳 순서다. 따라서 `01test'가 `02test'보다 먼저 실행된다. 현재는 다섯 개의 테스트 디렉토리가 있다.

00netfilter/

General netfilter framework tests.

01iptables/

iptables tests.

02conntrack/

connection tracking test.

03NAT/

NAT tests.

04ipchains-compat/

ipchains/ipfwadm compatibility tests.

testsuite/ 디렉토리 내부에는 `test.sh'라는 쉘 스크립트가 존재하며, 두개의 더미 인터페이스(tap0와 tap1)를 구성하여, forwarding을 on 시키고, 모든 netfilter 모듈을 제거하는 역할을 한다. 그리고 나서 하위 디렉토리에 존재하는 모든 쉘 스크립트를 수행하는 데 이중 하나라도 오류가 발생하면 수행을 멈추게 된다. `test.sh'는 두개의 옵션을 넘겨받을 수 있는 데, `-v'는 수행하는 각각의 테스트를 출력하며, 추가적인 test 이름이 주어지면 해당하는 테스트가 발견될 때까지 다른 테스트는 모두 건너뛰게 된다.

7.3. 유용한 도구들

``tools'' 디렉토리에는 쓸만한 testsuite 도구가 몇 개 있으며, 각 도구는 문제가 발생한 경우 non-zero exit status를 발생시키며 종료한다.


7.3.1. gen_ip

IP 패킷을 생성하는 함수로, IP 패킷을 표준 입력으로 출력한다. 여러분들은 표준 출력을 /dev/tap0와 /dev/tap1으로 전송하여 tap0와 tap1에 패킷을 보낼 수 있다. (/dev/tap0와 /dev/tap1이 존재하지 않으면 testsuite가 최초 실행 될 때 생성된다.)

FRAG=offset,length

패킷을 생성하고, offset과 length 만큼 패킷을 조각 낸다.

MF

패킷의 `More Fragments' 비트를 설정한다.

MAC=xx:xx:xx:xx:xx:xx

패킷에 소스 MAC 주소를 설정한다.

TOS=tos

패킷에 대하여 TOS 필드를 설정한다. 0-255.

다음은 강제 옵션이다.

source ip

패킷의 소스 IP 주소

dest ip

패킷의 목적지 주소

length

헤더를 포함한 패킷의 길이

protocol

패킷의 프로토콜 번호, (예: 17 = UDP)

각 전달인자는 프로토콜에 따라 달라진다. UDP(17)인 경우 소스와 목적지 포트 번호가 된다. ICMP(1)인 경우는 ICMP 메시지의 코드가 된다. 타입이 0 또는 8(ping-reply 또는 ping)인 경우는, 두개의 인자로 ID와 sequence 필드가 더 필요하다. TCP인 경우는 소스와 목적지 포트 그리고 플랙(``SYN'', ``SYN/ACK'', ``ACK'', ``RST'' 또는 ``FIN'')이 필요하다. 세개의 추가 옵션이 있는 데, ``OPT='' 다음에는 컴마로 분리된 옵션들이 오고, ``SYN='' 다음에는 시퀀스 번호, ``ACK='' 다음에도 시퀀스 번호가 호다. 마지막으로, ``DATA=''이라는 추가 옵션은 표준입력의 내용을 가득 채운 TCP 패킷의 부하를 의미한다.

7.4. 생각나는 대로 하는 충고

모든 도구는 읽고 쓰기를 한번 할 때 모든 것을 할 수 있다고 가정한다. 즉 ethertap 장치에 대해서는 사실이지만, pipe를 이용하여 약간 복잡한 작업을 할 때는 반드시 옳다고 말할 수는 없다.

패킷을 자르기 위해서 dd를 사용할 수 있다. dd는 일회성 작업으로 패킷을 출력하기 위해 사용할 수 있는 obs(output block size)라는 옵션이 있다.

첫 번째에 성공하도록 테스트하는 것이 좋다. 즉, 패킷이 성공적으로 블럭 되도록 테스트해야 한다. 처음에는 패킷이 정상적으로 통과하도록 테스트를 하고 나서, 일부 패킷이 블럭 되도록 테스트해야 한다. 그렇지 않으면, 엉뚱한 오류로 인해 패킷이 진행하는 것을 중지시킬 수 도 있다.

아무거나 막 보내고 나서 어떤 결과가 일어나는 확인하지 말고, 정확한 테스트를 할 수 있도록 해야한다. 정확한 테스트가 잘 못된 경우라면, 무엇이 잘 못 되었는지 알 수 있지만, 랜덤 테스트가 잘못 되면 전혀 도움이 되지 않는다.

아무런 메시지도 남기지 않고 테스트가 실패한 경우, 어떤 명령이 실행되고 있는지 알아보기 위해 스크립트의 맨 위에 `-x'를 추가 할 수 있다. 즉, `#!/bin/sh -x'.

테스트가 불규칙적으로 실패하면, 모든 외부 장치를 차단하거나 하는 식으로, 랜덤 네트웍 트래픽에 대하여 확인해 보는 것이 좋다. 예를 들면,Andrew Tridgell처럼 동일한 네트웍 상에 있으면, 윈도우의 브로드 캐스팅에 의해 수많은 간섭을 받게 된다.


8. 개발 동기

필자가 ipchains를 개발하고 나서, 시드니의 중국식당 입구에 들어서려는 순간 불현듯 머리는 스치는 생각에 패킷 필터링이 엉뚱한 곳에서 이루어 지고 있다는 사실을 깨달았다. 지금은 어디인지 찾을 수 없지만, 한가지 기억하는 것은 Alan Cox에게 메일을 보냈고, 친절하게도 그는 `처음엔 아무리 당신이 옳았다고 할지라도 한일을 끝내는 것이 어떤가요?'라는 답장을 보냈다. 이 짧은 말 한마다에서 실용주의가 정도를 이겨버렸다.

최초에는 ipfwadm의 커널 파트의 마이너 수정이었지만 결국에는 많은 부분을 다시 작성하여 ipchains를 마무리짓고 HOWTO를 쓰고 나서야, 패킷 필터링, 매스커레이딩, 포트포워딩과 유상한 여러 이슈에 대하여 리눅스 커뮤니티에서 많은 혼란이 있었다는 것을 알게 되었다.

이런 것들이 바로 여러분들의 도움으로부터 얻는 즐거움이다. 여러분들은 사용자들이 무엇을 하려고 하고, 또 무엇을 가지고 논쟁하는 지 보다 가까이서 느낄 수 있다. 프리소프트웨어라는 것은 많은 사용자들이 사용하고 있을 때 가치가 있고, 보다 쉽게 만들 수 있다는 것을 의미한다. 문서가 아닌 구조(architecture)가 최대 결함이었다.

그래서 필자는 ipchains 코드를 가지고 사람들이 하려고 했던 아이디어와 경험을 얻었다. 문제는 단지 둘 뿐이었다.

첫 번째로, 필자는 보안 쪽으로 다시 돌아가고 싶지 않았다. 보안 컨설턴트가 된다는 것은 여러분의 양심과 부의 사이에 있는 영원한 도덕적인 전쟁이다. 기본적인 수준에서, 여러분들은 실제 보안이 아닌 보안에 대한 분위기를 팔고 있는 것이다. 보안을 이해하고 있는 군대에서 군사용으로 작업을 하더라고, 다를 바는 없다.

두 번째 문제는 새로운 사용자들은 관심 밖의 문제였다. 큰 회사와 ISP들이 앞다퉈 이 물건을 사용하고 있다. 내가 필요한 것은, 물론 그들이 내일의 홈유저의 규모가 될 수도 있지만, 이런 류의 사용자들 부터의 신뢰를 받는 것이었다.

WatchGuard에 대한 노출이 내게 필요한 대규모의 클라이언트의 노출을 가져다 주었고, 그들로부터 독립하고자 하는 것이 모든 사용자들에게 동일하게 지원되도록 하였다.

그래서 필자는 쉽게 작성된 netfilter를 가질 수 있었고, 최상위에 ipchains를 포팅 하여, 이를 가지고 모든 것을 해냈다. 불행하게도 커널에서 모든 매스커레이딩 코드가 사라져 버렸다. 즉, 필터링으로부터 매스커레이딩을 독립시키고자 하는 것이 패킷 필터링으로 이동하게된 주된 이유이지만, 그렇게 함으로써 매스커레이딩 역시 netfilter 프레임 웍으로 이동할 필요가 있었다.

ipfwadm의 `interface-address'(ipchains에서 필자가 삭제한 것 중 하나)의 특성에 대한 나의 경험으로 배운 것은, 단순히 매스커레이딩 코드를 분리하고 나를 대신하여 netfilter에 포팅해 줄 사람을 기다리는 것은 아무런 희망이 없다는 것이다.

그래서 필자는 최소한 현재 코드만큼 많은 특성을 확보할 필요가 있었다. 초기에 도입한 niche users를 북돋기 위해, 정확히 말하면 조금 더 필요했다. 이는 투명한 프락시, 매스커레이딩과 포트 포워딩을 대체한다는 것을 의미한다. 바꿔 말하면, 완벽한 NAT 계층이 되는 것이다.

일반적인 NAT 시스템을 작성하는 대신, 기존의 매스커레이딩 계층을 포팅하기로 결정했었지만, 매스커레이딩은 연륜과 유지보수의 부재가 문제시되었다. 그러니까 매스커레이딩 메인터너가 없다는 이야기다. 순수한 사용자들은 매스커레이딩을 사용하지 않고, 유지보수 작업을 하려고 하는 홈 유저들도 많지 않다. Juan Ciarlante 같이 용감한 사람들이 버그를 고쳤지만, 결국 다시 작성해야 하는 수준에 이르게 되었다.

필자는 NAT를 다시 작성한 사람이 아니라는 것을 알아주기 바란다. 즉 필자는 더 이상 매스커레이딩을 사용하지 않고, 당시 기존의 코드를 살펴보지도 않았다. 아마도 이것이 내가 생각한 것보다 많은 시간이 소요된 이유일 것이다. 하지만 필자의 견해로는 결과는 꽤 좋았고, 많은 것을 배울 수 있었다. 얼마나 많은 사람들이 사용하고 있는 지 알게 되면, 두 번째 버전은 훨씬 더 좋아 질 것이라는 점은 믿어 의심치 않는다