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.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당 하나의 인터럽트에 대해 하나의 패킷만 존재하게 된다.

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

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.1.3. 새로운 Tables

여러분들의 목적에 맞는 새로운 table을 작성할 수 있으며, 이를 위해서는 `ipt_register_table()'함수를 호출해야한다. 이 함수는 전달인자로 `struct ipt_table'을 받으며 그 구조는 다음과 같다.

list

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

name

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

table

`struct ipt_replace'로 가득 찬 필드로, 테이블을 교체하기 위하여 사용자 공간에서 사용된다. `counters' 포인터는 NULL로 설정되어야 한다. 이 구조체는 `__initdata'로 선언되기 때문에 부팅 후에는 초기화된다.

valid_hooks

테이블로 진입하고자 하는 IPv4 넷필터 훅의 비트매스크이다. 진입엔트리가 유효한지 확인하고, ipt_match와 ipt_target의 `checkentry()'함수에 대해 사용 가능한 훅을 계산하기 위해 사용한다.

lock

전체 테이블에 대하여 읽고 쓰기가 가능한 spinlock이며, RW_LOCK_UNLOCKED로 초기화 된다.

private

ip_tables 코드에 의해 내부적으로 사용된다.

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.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 함수가 있다.

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.1. 표준 NAT Targets

NAT target은 'nat' 테이블 내에서만 사용한다는 것만 제외하면, 여타의 iptables target extension과 상당히 유사하다. SNAT와 DNAT 모두 부가 데이터로서 `struct ip_nat_multi_range'를 취하고, 이 데이터는 맵핑으로 바인딩 하는 주소의 범위를 명시하게 된다. 범위 요소인 `struct ip_nat_range'는 최소/최대 IP와 최대/최소 프로토콜 값(예:TCP 포트)으로 구성된다. 플랙을 위한 공간도 있으며, 어떤 플랙은 IP주소가 맵팽될 수 있는 지 없는지 알려주고, 어떤 것은 범위의 protocol-specific 부분이 유효한지 알려주는 역할을 한다.

다중 범위는 `struct ip_nat_range'의 배열이며, 범위를 ``1.1.1.1-1.1.1.2 ports 50-55 AND 1.1.1.3 port 80''과 같이 설정할 수 있다는 것을 의미한다.

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.3. 새로운 NAT Targets

매우 흥미로운 부분으로, 새로운 맵핑 타입을 제공하는 새로운 NAT target을 여러분들이 작성할 수 있다. 기본 패키지에는 추가 target은 MASQUERADE와 REDIRECT으로 새로운 NAT target을 작성하기에 충분하리 만큼 쉽게 설명되어 있다.

위의 두 target은 다른 iptables target처럼 작성되어 있지만, 내부적으로는 connection을 추출하고 `ip_nat_setup_info()'를 호출한다.

4.4.4. 프로토콜 도우미(protocol helper)

connection tracking에 대한 protocol helper는 connection code가 다중 네트웍 connection을 사용하는 프로토콜을 알아차리고 초기 연결에 관련된 `child' connection을 표시할 수 있도록 하며, 일반적으로 이와 같은 과정은 data stream 외부의 관련된 주소를 읽음으로써 수행된다.

NAT에 대한 protocol helper는 다음과 같은 두 가지 작업을 수행한다. 첫 째로는 NAT 코드가 데이터 스트림을 포함하는 주소를 변경하도록 데이터 스트림을 처리할 수 있도록 한다. 두 번째로는 데이터 스트림이 들어올 때 그와 연관된 connection에 대하여 원래의 connection을 기초로 하여 NAT를 수행한다.

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

4.4.5.1. 설명

connection tracking 모듈의 임무는 어떤 패킷이 이미 이루어진 connection에 속해 있는 지를 명시하는 것으로, 다음과 같은 일을 한다.

  • 우리 모듈이 어떤 패킷에 관심을 가지고 있는 가를 netfilter에게 알려준다.(대부분의 helper는 특정한 포트에 대해 작업을 한다.)

  • netfilter에 함수를 등록한다. 앞서 언급한 범주에 속하는 모든 패킷에 대하여 등록된 함수가 호출된다.

  • 등록된 곳으로부터 호출되는 `ip_conntrack_expect_related()' 함수는 netfilter에게 연관된 connection을 예측할 수 있도록 알려준다.

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

여러분들이 작성한 커널 모듈의 init 함수는 `struct ip_conntrack_helper'에 대한 포인터를 가지고 `ip_conntrack_helper_register()' 함수를 호출해야만 한다. 이 구조체는 다음과 같은 필드를 가지고 있다.

list

링크드 리스트에 대한 헤더로, 넷필터가 내부적으로 다루는 리스트이다. `{ NULL, NULL}'로 초기화시킨다.

tuple

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

mask

역시 `struct ip_conntrack_tuple'이며, tuple의 어느 비트가 유효한지를 명시하고 있는 매스크이다.

help

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

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.1. 설명

NAT helper 모듈은 특정 응용프로그램에 적합한 NAT 핸들링을 수행한다. 이 함수는 데이터에 대한 on-the-fly 처리를 포함하고 있다. FTP의 포트 명령을 고려해보자. 이 때, 클라이언트는 서버에게 어떤 IP와 포트로 연결을 해야 하는 가를 물어보게 되고, FTP 제어 연결에서 PORT 명령이 수행된 후 FTP helper 모듈은 IP/port를 교체한다.

TCP를 다루는 경우는 사뭇 복잡해진다. 이유는 패킷 크기가 변하기 때문이다. FTP의 예를 다시 들어보면, PORT 명령이 수행된 후 IP/port tuple을 나타내는 스트링의 길이가 변할 것이다. 결국 패킷 크기가 변경되면, NAT 박스의 좌측과 우측간에 syn/ack의 차이가 발생할 것이다. 예를 들어보면, 패킷을 4 octet만큼 확장했다면, 이후 계속되는 패킷의 TCP sequence number에 앞서 확장시킨 만큼의 offset를 더해야만 한다.

연관된 모든 패킷에 대하여 특별한 NAT 처리가 필요한 경우도 있다. 다시금 FTP를 예로 들어보자. control connection의 PORT명령을 수행하는 클라이언트에 의해 얻어진 IP/port에 대해 DATA connection의 모든 입력 패킷은 정상적인 table lookup을 거치는 것보다는 반드시 NAT되어야만 한다.

  • 연관된 connection을 유발하는 패킷에 대한 callback(foo_help)

  • 연관된 모든 패킷에 대한 callback(foo_nat_expected)

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.5. Netfilter의 이해

Netfiler는 상당히 단순하며, 앞 절에서 꽤 상세히 설명하였다. 그러나, 간혹 NAT나 ip_tables 하부 구조가 제공하는 것 이외의 것 또는 여러분들이 전부 바꾸고 싶은 것에 대하여 알아볼 필요가 있다.

미래의 이야기가 되겠지만, netfilter가 지향하고 있는 중요한 쟁점 중 하나는 캐슁이다. 각각의 skb는 `nfcache' 필드를 가지고 있으며, 이는 헤더의 어떤 필드를 검사하고 패킷을 변경할 것인지 말 것인지에 대한 비트 매스크이다. 각각의 훅이 그와 연관된 비트에 대한 netfilter의 OR를 0으로 만드는 것이 아이디어로, 이렇게 함으로써 패킷이 netfilter를 거쳐야 할 이유가 전혀 없다는 것을 알아차릴 수 있는 아주 훌륭한 캐쉬 시스템을 향후 작성할 수 있다.

가장 중요한 비트는 NFC_ALTERD와 NFC_UNKNOWN으로, NFC_ALTERED는 패킷이 변경되었다는 것을 의미하며 이 비트는 변경된 패킷을 다시 라우팅하기 위해 IPv4의 NF_IP_LOCAL_OUT 훅에 대하여 이미 적용되었다. NFC_UNKNOWN은, 표현할 수 없는 어떤 특성이 검출되어 캐슁이 수행되지 않았다는 것을 의미한다. 만일 의심이 가는 경우가 발생하면, 여러분의 훅 내부에 있는 skb의 nfcache 필드에 대해 NFC_UNKNOWN 플랙을 설정하기만 하면 된다.

4.6. 새로운 Netfilter 모듈 작성

4.6.1. Netfilter 훅에 연결하기

커널 내부에서 패킷을 줄이거나 조각내기 위해서는 `nf_register_hook' 함수와 `nf_unregister_hook' 함수를 사용하면 된다. 이들 각각은 다음과 같은 필드를 포함하는 `struct nf_hook_ops'에 대한 포인터를 인자로 취한다.

list

링크드 리스트로 `{ NULL, NULL }'로 설정된다.

hook

패킷이 이 훅 포인트에 걸리면 호출되는 함수로, NF_ACCEPT, NF_DROP 또는 NF_QUEUE 중 하나를 반드시 리턴 해야 한다. NF_ACCEPT인 경우는 현재의 포인터 다음에 오는 훅이 호출된다. NF_DROP인 경우는 패킷이 DROP되고, NF_QUEUE인 경우는 대기열로 들어 간다. skb 포인터에 대한 포인터를 돌려 받아서 원하는 경우에는 skb를 전부 바꾸어 버릴 수도 있다.

flush

현재는 사용하지 않고 있다. 캐쉬가 지워지는 경우 패킷 적중률을 전달하기 위해 설계되었으나, 전혀 구현될 일이 없을 것이다. 그냥 NULL로 설정하기 바란다.

pf

프로토콜 패밀리, 즉 IPv4에 대해서는 `PF_INET'이 된다.

hooknum

관심을 가지고 있는 훅의 수, 즉 `NF_IP_LOCAL_OUT'이다.

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'를 살펴보면, 패킷에 대한 부가적인 정보, 즉 패킷이 존재했던 인터페이스와 훅 같은 것을 얻을 수 있다.

4.6.3. 사용자 공간으로부터 명령어 전달받기

Netfilter의 구성요소들이 사용자공간과 상호작용을 필요로 한다는 것은 아주 당연한 일이다. 방법은 setsockopt 메커니즘을 사용하여 이런 작용을 구현할 수 있다. 여기서 주의할 점은 각 프로토콜이 이해하지 못하는 setsockopt 넘버에 대해 Nf_setsockop()를 호출할 수 있도록 각 프로토콜이 수정되어야만하며, 이는 IPv4까지 이고, IPv6와 DECnet은 이미 변경되어 있다.

최근에 알려진 기술을 사용하면, nf_register_sockopt() 함수를 사용하여 `struct nf_sockopt_ops'를 등록하며, 이 구조체는 다음과 같은 필드로 구성되어 있다.

list

링크드 리스트를 사용하기 위한 것으로, `{ NULL, NULL }'로 설정된다.

get_optmin, get_optmax

처리해야 할 getsockopt의 개수의 범위를 지정한다. 즉 0과 0을 사용하면 처리해야 할 getsockopt의 개수가 없다는 것을 의미한다.

get

사용자들이 여러분들이 작성한 getsockopt 중 하나를 호출한 경우 호출되는 함수이다. 이 함수 내부에서 사용자들이 NET_ADMIN의 권한을 가지고 있는 지 확인해야 한다.

나머지 두개의 필드는 내부적으로 사용된다.

4.7. 사용자 공간에서 패킷 처리

libipq 라이브러리와 `ip_queue' 모듈을 사용하면, 커널에서 할 수 있는 대부분의 것들을 사용자 공간에서 수행할 수 있다. 이것이 다음과 같은 것을 의미한다. 속도에 대한 문제가 발생한다면, 사용자 공간에서 완전히 여러분들만의 코드를 개발할 수 있다. 개발하고자 하는 여러분들이 큰 대역을 필터링 하고자 하지만 않는다면, 커널 내부의 패킷 맹글링에 비해 이 방법이 월등하다는 것을 알게 될 것이다.

netfilter 초창기에, 필자는 iptables의 초기 버전을 포팅 하여 이를 증명하였다. netfilter는, 개발자들이 원하는 언어가 무엇이던 간에, 개발자 자신의 코드와 고효율의 넷맹글링 모듈을 개발하고자 하는 사람들에게 오픈 되어 있다.