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). 세 번째 부분은 대기된 패킷을 사용자 공간으로 보내기 위해 제어하는 것으로 이러한 패킷은 비동기방식으로 처리된다. 마지막 부분은 코드와 문서에 기록된 주석문으로 구성되어 있으며, 이는 어떠한 실험적 프로젝트에 대해서도 도움이 되는 부분이다. 넷 필터의 모토는 다음과 같다.
이러한 저수준 프레임웍과 더불어, 다양한 모듈이 작성되었으며, 이는 이전 버전의 커널에 대하여 유사한 기능, 확장 가능한 NAT시스템 그리고 확장 가능한 패킷 필터링 시스템을 제공한다. 1.1.1. 커널 2.0과 2.2에서의 문제점?
1.1.2. 누구시죠?나는 이런 짓을 하리만큼 바보스러운 사람이다. ipchains의 공동저자이고 현재 리눅스 커널 IP 방화벽의 메인터너로서, 현재의 시스템 때문에 사람들이 많은 문제를 격었다는 것 뿐만 아니라 그들이 시도하고 있는 것이 점점 더 노출되고 있다는 것을 알았다. 1.1.3. 그게 왜 폭주하죠?이~~야!, 지난 주에 이 문서를 봤어야 하는 건데... 사실 나는 우리 모두가 되기 원하는 그런 훌륭한 프로그래머가 아니고, 시간, 장비 그리고 영감도 부족해서 시나리오 전체를 충분히 테스트 해보지 못했다. 그저 내가 해본 것이라고는 여러분들이 참여하기를 바라는 마음에서 만든 testsuite를 돌리는 것이 고작이었다. 2. 어디서 최신 버전을 구하죠?최신의 HOWTO, userspace tools 그리고 testsuite를 가지고 있는 CVS 서버가 samba.org에 있다. 통상적인 브라우징 방법으로는, 웹 인터페이스를 사용할 수 있다. 최신의 소스를 얻으려면 다음과 같이 하면 된다:
3. 넷필터 아키텍처넷필터는 단지 프로토콜 스택의 다양한 포인트에 존재하는 훅의 연속일 뿐이다. 이상적인 IPv4의 진행경로 다이어그램은 다음과 같다.
다음으로, 패킷은 라우팅 코드로 들어가며, 여기서 패킷이 다른 인터페이스로 향하는지 또는 로컬 프로세스로 향하는지 결정된다. 패킷이 라우팅될 수 없는 경우, 라우팅 코드는 패킷을 버리기도 한다. 만일 패킷의 목적지가 들어온 박스라면, 넷필터 프레임웍은 패킷을 프로세스로 전달하기 전에 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.1. 넷필터의 기초이 절에서는 IPv4에 대한 넷필터의 예를 보일 것이며, 이를 통해 여러분들은 각각의 훅이 동작하는 시점을 이해하게 될 것이다. 이 것이 바로 넷필터의 기초이다. 커널 모듈은 앞서 언급한 어떠한 훅에 대해서 응답할 수 있도록 등록할 수 있으며, 어떤 함수를 등록한 모듈은 훅 내에서 함수의 우선순위에 대하여 반드시 명시하여야 한다. 코어 네트워킹 코드로부터 넷필터 훅이 호출되는 경우, 각 포인트에 등록된 각각의 모듈은 우선순위에 따라 호출이 되고, 패킷을 자유로이 다룰 수 있다. 모듈은 넷필터에게 다음의 다섯 가지 동작을 하도록 요청한다.
넷필터의 다른 부분은 뒤에 나오는 커널 부분에서 다루기로 한다. 이상과 같은 것이 기초가 되어, 저자들은 다음 두 절에 보인 것과 같은 복잡한 패킷 처리를 만들 수 있다. 3.2. 패킷 선택: IP TablesIP table을 호출하는 패킷 선택 시스템은 넷필터 프레임웍을 기반으로 구성되었으며, 이는 확장성을 가지고 ipchains로부터 직접 물려받은 유산이다.(ipchains는 ipfwadm으로부터 물려받고, ipfwadm은 BSD의 ipfw IIRC로부터 물려받았다.) 커널 모듈은 새로운 테이블을 등록할 수 있으며, 임의의 패킷이 주어진 테이블을 통과하도록 요청할 수 있다. 이러한 패킷 선택 방법은 패킷 필터링(즉 `필터' 테이블), 네트웍 주소 변환(`nat' 테이블) 그리고 종합적인 pre-route 패킷 mangling(`mangling' 테이블')에 사용한다. 넷필터에 등록된 훅은 다음과 같다.(여기서는 각각의 함수가 실제로 호출되는 순서로 각각의 훅에 있는 함수와 같이 보였다.)
3.2.1. 패킷 필터링`filter'라는 테이블은 패킷을 절대로 변경시키지 않고 단지 걸러내기만 한다. ipchains와 비교했을 때, iptables filter의 장점 중 하나는 작고 빠르다는 것이며, NF_IP_LOCAL_IN과 NF_IP_FORWARD, NF_IP_LOCAL_OUT 시점에서 넷필터로 훅킹된다. 이는, 주어진 패킷에 대하여 이를 필터링 하는 위치가 오직 하나 뿐이라는 것을 의미한다. 결국 사용자들이 ipchains를 사용한 것보다 더 단순하게 만들며, 또한 넷필터 프레임웍이 NF_IP_FORWARD 훅에 대하여 입력과 출력 인터페이스를 제공한다는 사실은 다양한 필터링이 훨씬 단순해진다는 것을 의미한다. 주: 필자는 업그레이드의 필요 없이 기존의 ipfwadm과 ipchains를 사용 가능하도록 넷필터의 최상위에 모듈로서 ipchains와 ipfwadm의 커널부분을 포팅 하였다. 3.2.2. NAT이는 `nat' 테이블의 영역으로, 두 가지의 넷필터 훅으로 패킷을 전달한다. 즉, non-local 패킷에 대해, 각각 목적지와 소스 전환에 대하여 NF_IP_PRE_ROUTING과 NF_IP_POST_ROUTING 훅이 완벽하게 동작한다. CONFIG_IP_NF_NAT_LOCAL이 정의된 경우, NF_IP_LOCAL_OUT과 NF_IP_LOCAL_IN 훅이 로컬 패킷의 목적지를 전환하기 위해 사용된다. 이 테이블은 `filter' 테이블과는 약간 다르며, 새로운 커넥션의 첫 번째 패킷만이 테이블로 전달된다. 따라서 이와 같은 전달의 결과는 동일한 커넥션에 있어서 향후 전달되는 모든 패킷에 적용된다. 3.2.3. 매스커레이딩, 포트 포워딩, 투명한 프락시필자는 NAT를 출발지 NAT(즉 첫 번째 패킷이 출발지를 변경하는 경우)와 목적지 NAT(첫 번째 패킷이 목적지를 변경하는 경우)로 구분하였다. 매스커레이딩은 출발지 NAT의 특별한 경우이며, 포트 포워딩과 투명한 프락시는 목적지 NAT의 특별한 경우이다. 이와 같은 것은 서로 독립적인 엔티티를 가지고 NAT 프레임웍을 이용하여 구현되었다. 3.2.4. 패킷 맹글링(packet mangling)패킷 맹글링 테이블(`mangling' table)은 패킷의 정보를 실제로 변경하기 위해 사용되며, NF_IP_PRE_ROUTING과 NF_IP_LOCAL_OUT 시점에서 넷필터로 훅킹된다. 3.3. 연결 추적연결추적(connection tracking)은 NAT의 기본이지만, 모듈로 분리되어 구현된다. 이는 연결추적을 단순하고 명확하게 사용할 수 있도록 패킷 필터링 코드에 대한 확장성을 제공한다.(`state' 모듈) 3.4. 그이외 추가된 사항새로운 유연성 때문에 정말로 무서운 일을 겪을 기회가 많아졌지만, 다른 사람들이 향상된 코드를 작성하거나 기존의 코드를 완전히 대체해 버릴 수 있는 기회 역시 많아졌다. 4. 프로그래머들을 위한 정보비밀을 하나 말씀드리겠습니다. 뭐냐하면, 제가 기르는 햄스터가 모든 코드를 작성했습니다. 저는 단지 전달하는 역할만 했고, 모든 계획은 제 애완동물이 했습죠. 그러니 버그가 생기더라도 저를 원망하지 마시고, 귀여운 털북숭이를 원망하시기 바랍니다. 4.1. ip_tables의 이해iptables는 메모리 내에 있는 규칙의 명명된 배열과 각각의 훅으로부터 패킷이 전달되기 시작해야 하는 정보를 단순히 제공만 하는 것이다. 어떤 테이블이 등록되고 나면, 사용자 공간은 getsockopt()과 setsockopt()를 이용하여 그 내용을 읽고 변경할 수 있다. iptables는 어떠한 넷필터 훅에도 등록하지 않으며, 이를 수행하는 다른 모듈에 의존하고 적절히 패킷을 모듈에 전달한다. 다시 말해, 하나의 모듈은 넷필터 훅과 ip_tables에 따로따로 등록해야한 하고, 훅이 발생하면 ip_tables를 호출하는 메커니즘을 제공한다. 4.1.1. ip_tables의 데이터 구조편리성을 위해, 동일한 데이터 구조를 사용하여 사용자 공간에 의한 규칙과 커널내부의 규칙을 표현하였다. 이렇게 표현된 데이터 구조 중 아주 일부분만이 커널 내부에서 사용된다. 각각의 규칙은 다음과 같은 부분으로 구성된다.
`struct ipt_entry'는 다음과 같은 필드를 포함한다.
`struct ipt_entry_match'와 `struct ipt_entry_target'은 상당히 유사하며, 전체(IPT_ALIGN으로 정렬된) 길이 필드(각각 `match_size'와 `target_size')와, match와 target(사용자 공간에 대한) 명칭을 포함하는 구조체, 그리고 (커널에 대한) 포인터를 포함한다. 4.1.2. ip_tables의 사용과 진행커널은 특정한 훅에 의해 지시된 위치에서 관찰을 시작하여, 그에 관련한 규칙을 검사하고, `struct ipt_ip'의 element가 일치하는 경우, 차례로 각각의 `struct ipt_entry_match'를 검사한다(match가 호출된 곳과 관련한 match function을 수행한다). match function이 0을 돌려주는 경우, 현재 규칙에 대한 반복을 중단한다. `hotdrop' 파라미터가 1로 설정된 경우, 현재 패킷은 즉시 폐기된다(tcp match 함수와 같은 곳에서 조금이라도 수상한 패킷에 대해 사용한다). 수행반복이 끝까지 진행된 경우, 카운터가 증가하고, `struct ipt_entry_target'이 검사된다. 표준 타깃인 경우, `verdict' 필드를 읽는다. `verdict' 필드가 음인 경우 패킷이 결정된 것을 의미하고 양인 경우는 이동해야할 offset을 의미한다. 응답이 양이고 offset이 다음 규칙을 가리키지 않으면, `back' 이라는 변수가 세트되고 이전의 `back' 값이 현재 규칙의 `comefrom' 필드에 설정된다. 비표준 타깃에 대해서는 target 함수가 호출되어, 결정을 알려주게 된다.(비표준 타깃은 정적 루프 검출 코드를 위반하기 때문에 이동할 수 없다) 그 결정은, 다음 규칙으로 계속 진행하기 위해서는 IPT_CONTINUE가 될 것이다. 4.2. iptables 확장하기전 무지하게 게으른 넘이라서, iptables는 얼마든지 확장 가능합니다. 다시 말하면 제 손을 떠나 다른 사람에게 넘어간, 오픈소스 그 이상이라는 거죠. iptables를 확장한다는 것은 다음의 두 부분을 포함한다. 즉, 새로운 모듈을 작성하여 커널을 확장하는 것과 새로운 공유 라이브러리를 작성하여 사용자 차원의 프로그램인 iptables를 확장하는 것이다. 4.2.1. 커널예제를 보신 사람들은 알겠지만, 커널 모듈을 작성한다는 것 자체는 상당히 단순하다. 한가지 알아야 할 것은 여러분의 코드가 재진입 가능해야 한다는 것이다. 예를 들어보면, 사용자 공간으로부터 들어오는 어떤 패킷이 존재할 수 있을 것이며, 동시에 또 다른 패킷이 인터럽트에 의해 들어 올 수도 있을 것이다. 하지만, 커널 2.3.4이상에서 SMP를 사용할 경우, CPU당 하나의 인터럽트에 대해 하나의 패킷만 존재하게 된다. 여러분들이 알아야 하는 함수는 다음과 같다.
여러분이 작성한 새로운 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 구조체이다. 이 구조체는 다음과 같은 필드를 포함한다.
4.2.1.2. 새로운 Targets여러분의 타깃이 패킷(헤더나 바디)을 변경시킨다면, 패킷이 복제되는 시점에서 패킷을 복사하기 위하여 skb_unshare()를 호출해야만 한다. 그렇지 않으면 skbuff에 복제된 패킷이 있는 어떠한 raw socket이라도 변경된 사항을 알아차리게 된다. 새로운 target은 통상적으로 단독 모듈로 작성된다. `New Match Functions'의 절에서 언급한 바와 동일한 내용이 여기서도 적용된다. 여러분의 새로운 target의 핵심은 ipt_register_target()으로 전달되는 struct ipt_target으로, 이 구조체는 다음과 같은 필드를 갖고 있다.
4.2.1.3. 새로운 Tables여러분들의 목적에 맞는 새로운 table을 작성할 수 있으며, 이를 위해서는 `ipt_register_table()'함수를 호출해야한다. 이 함수는 전달인자로 `struct ipt_table'을 받으며 그 구조는 다음과 같다.
4.2.2. 사용자공간 도구(Userpace Tool)이제 여러분들이 직접 커널 모듈을 작성했고, 사용자 공간에서 이에 대한 옵션을 조정하기를 원할 것이다. 필자는 각각 확장된 버전에 대하여 새로이 파생된 버전을 만드는 것보다는 아주 최신의 90년대 기술을 사용한다. 즉 furbies이다. 쐬리... 공유라이브러리를 말하는 것이다. 새로운 테이블을 사용하고자 하는 경우 iptables를 확장할 필요는 없고, `-t' 옵션만 주면 된다. 공유라이브러리는 `_init()'함수를 포함해야하며, 이 함수는 모듈이 로딩 되는 시점에서 자동으로 호출된다. 여러분이 작성한 공유라이브러리가 새로운 match나 새로운 target을 포함하느냐에 따라 _init()함수가 `register_match()'나 `register_target()'함수를 호출한다. 공유라이브러리를 제공해야할 필요도 있으며, 구조체의 일부를 초기화하거나 추가 옵션을 제공하는 데 공유라이브러리를 사용할 수도 있기 때문이다. 필자는 공유라이브러리가 아무것도 안 할지라도 공유라이브러리는 라이브러리가 없을 때 발생하는 문제를 줄일 수 있기 때문에 반드시 공유라이브러리를 사용하기를 주장한다. `iptables.h' 헤더에 유용한 함수들이 정의되어 있으며, 그중 다음과 같은 것들이 상당히 유용하다.
4.2.2.1. 새로운 Match 함수여러분들이 작성한 공유라이브러리의 _init() 함수는 `register_match()'에 정적 구조체인 `struct iptables_match'에 대한 포인터를 넘겨준다. 이 구조체는 다음과 같은 필드를 포함한다.
4.2.2.2. 새로운 Targets여러분의 공유라이브러리의 _init() 함수는 `register_target()' 함수로 정적으로 선언된 `struct iptables_target'에 대한 포인터를 전달하며, 이는 앞서 언급한 iptables_match 구조체와 유사한 필드를 포함한다. 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 함수가 있다.
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)되어 있으며, 다음과 같은 값을 갖는다.
4.4. Connection Tracking/NAT 확장하기이 방식은 여러 가지 프로토콜과 서로 다른 맵핑 타입을 조절하기 위하여 설계된 것으로, 맵핑 타입 중 일부는 부하분산(load-balancing)이나 fail-over 맵팽 타입처럼 상당히 구체적인 것도 있다. 내부적으로는, connection tracking은 일치하는 룰이나 binding을 검색하기 전에 패킷을 ``tuple''로 변환시킨다. 여기서 ``tuple''이란 패킷 중 관심의 대상이 되는 부분을 말한다. ``tuple''은 처리 가능한 부분과 그렇지 못한 부분으로 구분되며, 각각 ``src''와 ``dst''라고 한다. 이는 Source NAT의 입장에서 첫 번째 패킷에 대한 관점이다. 동일한 방향 |