한참 전에 과제를 했지만 이제서야 이 글을 올리는 이유는
다시 진행되는 LTE 프로젝트를 스스로 이해하기 위함입니다.
이번 과제는 netfilter 코드를 사용하여 tcp 패킷만 출력하고 해당 패킷은 Drop 하는 것.
https://gitlab.com/gilgil/sns/-/wikis/netfilter/netfilter
해당 링크를 참고하여 과제가 나갔던 것으로 기억한다.
링크에 있는 코드를 기반으로 해서 과제를 수행했다.
https://github.com/63um3um/SF_Network_Study/tree/master/nf
코드 리뷰를 수행하기 전에 iptables와 netfilter가 뭔지 복습해야겠다.
iptables? Netfilter란?
Iptables
iptable은 리눅스에서 기본 방화벽으로 사용되며 이를 조작하는 것이 netfilter인듯.
iptables 방화벽은 규칙을 관리하기 위해서 테이블을 사용하고 기억할 것은 INPUT, OUTPUT이었는데
우선 iptables를 사용하려면 sudo를 붙여야 했고,
sudo iptables -L
chain안에 있는 모든 rule를 보여주낟.
sudo iptables -F
chain안에 있는 모든 rule을 삭제한다.
sudo iptables -A chain
chain을 추가한다.
sudo iptables -D chain
chain을 삭제한다.
예제
sudo iptables -A OUTPUT -p icmp -j DROP
밖으로 나가는 -p(protocol) icmp 프로토콜을 DROP한다
sudo iptables -F
sudo iptables -A INPUT -p icmp -j DROP
이미 생성된 모든 규칙을 삭제하고
INPUT되는 icmp 프로토콜을 DROP한다.
sudo iptables -F
sudo iptables -A OUTPUT -p tcp -j DROP
sudo iptables -A INPUT -p tcp -j DROP
sudo iptables -A OUTPUT -p tcp --dport 80 -j ACCEPT
sudo iptables -A INPUT -p tcp --sport 80 -j ACCEPT
위 코드는 tcp를 INPUT OUTPUT 모두 DROP할 것이다.
iptables는 우선 순위가 순서에 영향을 받는데 먼저 선언한 것이 우선되기 때문이다.
destination, source port를 지정해서 ACCEPT를 해도 무용지물일 것이다.
netfilter
sudo iptables -F
sudo iptables -A OUTPUT -j NFQUEUE --queue-num 0
sudo iptables -A INPUT -j NFQUEUE --queue-num 0
코드블럭처럼 명령어를 실행하면 송수신되는 IP packet을 모두 netfilter queue로 넘긴다.
queue-num은 기본적으로 0번
Code Review
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
#include <linux/types.h>
#include <linux/netfilter.h> /* for NF_ACCEPT */
#include <errno.h>
#include<libnet.h> //libnet을 이용하여 패킷 의미 찾기
#include <libnetfilter_queue/libnetfilter_queue.h>
int is_tcp = 0;
void dump(unsigned char* buf, int size) { // data와 ret를 확인하는 함수.패킷 데이터가 출력되는 것을 볼 수 있음. ip 헤더부터 시작
struct libnet_ipv4_hdr *ipv4 = (struct libnet_ipv4_hdr *) (buf); //libent 사용
struct libnet_tcp_hdr *tcp = (struct libnet_tcp_hdr *) (buf + sizeof(*ipv4));
uint32_t sip = htonl(ipv4->ip_src.s_addr);
uint32_t dip = htonl(ipv4->ip_dst.s_addr);
uint8_t ip1, ip2, ip3, ip4;
printf("===========================\n");
printf("Source IP: ");
ip1 = (sip & 0xff000000) >> 24;
ip2 = (sip & 0x00ff0000) >> 16;
ip3 = (sip & 0x0000ff00) >> 8;
ip4 = (sip & 0x000000ff);
printf("%d.%d.%d.%d\n",ip1, ip2, ip3, ip4);
printf("Destination IP: ");
ip1 = (dip & 0xff000000) >> 24;
ip2 = (dip & 0x00ff0000) >> 16;
ip3 = (dip & 0x0000ff00) >> 8;
ip4 = (dip & 0x000000ff);
printf("%d.%d.%d.%d\n",ip1, ip2, ip3, ip4);
if(ipv4->ip_p == 0x06)
{
printf("tcp\n");
is_tcp = 1;
printf("Source Port: ");
printf("%d\n",ntohs(tcp->th_sport));
printf("Destination Port: ");
printf("%d\n",ntohs(tcp->th_dport));
}
printf("===========================\n");
/*
int i;
for (i = 0; i < size; i++) {
if (i != 0 && i % 16 == 0)
printf("\n");
printf("%02X ", buf[i]);
}*/
printf("\n");
}
/* returns packet id */
static u_int32_t print_pkt (struct nfq_data *tb) //패킷 정보를print 하는 함수
{
int id = 0;
struct nfqnl_msg_packet_hdr *ph;
struct nfqnl_msg_packet_hw *hwph;
u_int32_t mark,ifi;
int ret;
unsigned char *data;
ph = nfq_get_msg_packet_hdr(tb);
if (ph) {
id = ntohl(ph->packet_id);
// printf("hw_protocol=0x%04x hook=%u id=%u ",
// ntohs(ph->hw_protocol), ph->hook, id);
}
hwph = nfq_get_packet_hw(tb);
if (hwph) {
int i, hlen = ntohs(hwph->hw_addrlen);
// printf("hw_src_addr=");
// for (i = 0; i < hlen-1; i++)
// printf("%02x:", hwph->hw_addr[i]);
// printf("%02x ", hwph->hw_addr[hlen-1]);
}
mark = nfq_get_nfmark(tb);
// if (mark)
// printf("mark=%u ", mark);
ifi = nfq_get_indev(tb);
// if (ifi)
// printf("indev=%u ", ifi);
ifi = nfq_get_outdev(tb);
// if (ifi)
// printf("outdev=%u ", ifi);
ifi = nfq_get_physindev(tb);
// if (ifi)
// printf("physindev=%u ", ifi);
ifi = nfq_get_physoutdev(tb);
// if (ifi)
// printf("physoutdev=%u ", ifi);
ret = nfq_get_payload(tb, &data); //중요한 함수, 패킷에 대한 시작 위치가 data에 들어가고 그 길이가 ret에 들어간다.
if (ret >= 0)
{
dump(data, ret);
}
// printf("payload_len=%d\n", ret);// ret는 패킷의 길이
// fputc('\n', stdout);
return id;
}
static int cb(struct nfq_q_handle *qh, struct nfgenmsg *nfmsg,
struct nfq_data *nfa, void *data)// call back fucntion
{
is_tcp = 0;
u_int32_t id = print_pkt(nfa);// 패킷 정보를 춫력하고 id를 넣어서
printf("entering callback\n");
if(is_tcp) // tcp : DROP
{
printf("DROP!\n\n");
return nfq_set_verdict(qh, id, NF_DROP, 0, NULL);
}
else
{
return nfq_set_verdict(qh, id, NF_ACCEPT, 0, NULL);
}
}
int main(int argc, char **argv) //main function
{
struct nfq_handle *h;
struct nfq_q_handle *qh;
struct nfnl_handle *nh;
int fd;
int rv;
char buf[4096] __attribute__ ((aligned));
printf("opening library handle\n");
h = nfq_open();
if (!h) { //h error
fprintf(stderr, "error during nfq_open()\n");
exit(1);
}
printf("unbinding existing nf_queue handler for AF_INET (if any)\n");
if (nfq_unbind_pf(h, AF_INET) < 0) {
fprintf(stderr, "error during nfq_unbind_pf()\n");
exit(1);
}
printf("binding nfnetlink_queue as nf_queue handler for AF_INET\n");
if (nfq_bind_pf(h, AF_INET) < 0) {
fprintf(stderr, "error during nfq_bind_pf()\n");
exit(1);
}
printf("binding this socket to queue '0'\n");
qh = nfq_create_queue(h, 0, &cb, NULL); //패킷이 queue에 들어오면 callback함수(cb) 호출
if (!qh) {
fprintf(stderr, "error during nfq_create_queue()\n"); //qh error
exit(1);
}
printf("setting copy_packet mode\n");
if (nfq_set_mode(qh, NFQNL_COPY_PACKET, 0xffff) < 0) {
fprintf(stderr, "can't set packet_copy mode\n");
exit(1);
}
fd = nfq_fd(h);
for (;;) {
if ((rv = recv(fd, buf, sizeof(buf), 0)) >= 0) { //뭔가 패킷을 받으면 에러나는듯(큐 벗어나서?)
printf("\n\npkt received\n");
nfq_handle_packet(h, buf, rv);
continue;
}
/* if your application is too slow to digest the packets that
* are sent from kernel-space, the socket buffer that we use
* to enqueue packets may fill up returning ENOBUFS. Depending
* on your application, this error may be ignored. nfq_nlmsg_verdict_putPlease, see
* the doxygen documentation of this library on how to improve
* this situation.
*/
if (rv < 0 && errno == ENOBUFS) {
printf("losing packets!\n");
continue;
}
perror("recv failed");
break;
}
printf("unbinding from queue 0\n");
nfq_destroy_queue(qh);
#ifdef INSANE
/* normally, applications SHOULD NOT issue this command, since
* it detaches other programs/sockets from AF_INET, too ! */
printf("unbinding from AF_INET\n");
nfq_unbind_pf(h, AF_INET);
#endif
printf("closing library handle\n");
nfq_close(h);
exit(0);
}
github에 제출한 것을 그대로 올려서 차근차근 봅시다.
구석구석 주석을 달아두긴 했는데 main부터 잘라서 보면
int main(int argc, char **argv) //main function
{
struct nfq_handle *h;
struct nfq_q_handle *qh;
struct nfnl_handle *nh;
int fd;
int rv;
char buf[4096] __attribute__ ((aligned));
printf("opening library handle\n");
h = nfq_open();
if (!h) { //h error
fprintf(stderr, "error during nfq_open()\n");
exit(1);
}
printf("unbinding existing nf_queue handler for AF_INET (if any)\n");
if (nfq_unbind_pf(h, AF_INET) < 0) {
fprintf(stderr, "error during nfq_unbind_pf()\n");
exit(1);
}
printf("binding nfnetlink_queue as nf_queue handler for AF_INET\n");
if (nfq_bind_pf(h, AF_INET) < 0) {
fprintf(stderr, "error during nfq_bind_pf()\n");
exit(1);
}
printf("binding this socket to queue '0'\n");
qh = nfq_create_queue(h, 0, &cb, NULL); //패킷이 queue에 들어오면 callback함수(cb) 호출
if (!qh) {
fprintf(stderr, "error during nfq_create_queue()\n"); //qh error
exit(1);
}
printf("setting copy_packet mode\n");
if (nfq_set_mode(qh, NFQNL_COPY_PACKET, 0xffff) < 0) {
fprintf(stderr, "can't set packet_copy mode\n");
exit(1);
}
fd = nfq_fd(h);
for (;;) {
if ((rv = recv(fd, buf, sizeof(buf), 0)) >= 0) { //뭔가 패킷을 받으면 에러나는듯(큐 벗어나서?)
printf("\n\npkt received\n");
nfq_handle_packet(h, buf, rv);
continue;
}
/* if your application is too slow to digest the packets that
* are sent from kernel-space, the socket buffer that we use
* to enqueue packets may fill up returning ENOBUFS. Depending
* on your application, this error may be ignored. nfq_nlmsg_verdict_putPlease, see
* the doxygen documentation of this library on how to improve
* this situation.
*/
if (rv < 0 && errno == ENOBUFS) {
printf("losing packets!\n");
continue;
}
perror("recv failed");
break;
}
printf("unbinding from queue 0\n");
nfq_destroy_queue(qh);
#ifdef INSANE
/* normally, applications SHOULD NOT issue this command, since
* it detaches other programs/sockets from AF_INET, too ! */
printf("unbinding from AF_INET\n");
nfq_unbind_pf(h, AF_INET);
#endif
printf("closing library handle\n");
nfq_close(h);
exit(0);
}
main 함수에서 nfq_handle 포인터 h로 선언하고, nfq_q_handle 포인터 qh, nfnl_handle 포인터 nh를 선언.
정수형으로 fd, rv 선언, char 형 배열 buf선언
h 에는 nfq_open()의 반환값이 들어감
나머지는 에러에 대한 것인듯
위 프로그램을 실행시키기 위해서
sudo iptables -F
sudo iptables -A OUTPUT -j NFQUEUE --queue-num 0
sudo iptables -A INPUT -j -NFQUEUE --queue-num 0
들어오는 패킷을 큐로 받아들이는 과정이 필요
h를 검사하는 과정이 끝나면 fd에 nfq_fd(h) 반환값을 대입
qh에 nfq_create_queue(h, 0, &cb, NULL) 반환값 대입. 패킷이 queue에 들어오면 callback 함수를 호출한다고 함
static int cb(struct nfq_q_handle *qh, struct nfgenmsg *nfmsg,
struct nfq_data *nfa, void *data)// call back fucntion
{
is_tcp = 0;
u_int32_t id = print_pkt(nfa);// 패킷 정보를 춫력하고 id를 넣어서
printf("entering callback\n");
if(is_tcp) // tcp : DROP
{
printf("DROP!\n\n");
return nfq_set_verdict(qh, id, NF_DROP, 0, NULL);
}
else
{
return nfq_set_verdict(qh, id, NF_ACCEPT, 0, NULL);
}
}
cb(callback)함수 static으로 선언되어 있음. 프로젝트로 옮길 때 static이 아니라서 에러가 발생했는데 그냥 그런갑다 하고 static의 만들어진 진짜 용도가 뭔지는 잘..
cb함수에서는 is_tcp라는 변수를 0으로 초기화 함... 형 안썼는데 어떻게 에러가 안 났지..? (내가 추가한건데)
-> 라고 생각했으나 전역변수였구연
unsigned int 크기의 변수 id를 선언하고 print_pkt(nfa)를 대입한다.
callback 함수는 어떻게 실행되는지 모르겠다. 어디서 매개변수를 받아오는거지. 아무래도 create_queue에서 받는 것 같은데.
static u_int32_t print_pkt (struct nfq_data *tb) //패킷 정보를print 하는 함수
{
int id = 0;
struct nfqnl_msg_packet_hdr *ph;
struct nfqnl_msg_packet_hw *hwph;
u_int32_t mark,ifi;
int ret;
unsigned char *data;
ph = nfq_get_msg_packet_hdr(tb);
if (ph) {
id = ntohl(ph->packet_id);
// printf("hw_protocol=0x%04x hook=%u id=%u ",
// ntohs(ph->hw_protocol), ph->hook, id);
}
hwph = nfq_get_packet_hw(tb);
if (hwph) {
int i, hlen = ntohs(hwph->hw_addrlen);
// printf("hw_src_addr=");
// for (i = 0; i < hlen-1; i++)
// printf("%02x:", hwph->hw_addr[i]);
// printf("%02x ", hwph->hw_addr[hlen-1]);
}
mark = nfq_get_nfmark(tb);
// if (mark)
// printf("mark=%u ", mark);
ifi = nfq_get_indev(tb);
// if (ifi)
// printf("indev=%u ", ifi);
ifi = nfq_get_outdev(tb);
// if (ifi)
// printf("outdev=%u ", ifi);
ifi = nfq_get_physindev(tb);
// if (ifi)
// printf("physindev=%u ", ifi);
ifi = nfq_get_physoutdev(tb);
// if (ifi)
// printf("physoutdev=%u ", ifi);
ret = nfq_get_payload(tb, &data); //중요한 함수, 패킷에 대한 시작 위치가 data에 들어가고 그 길이가 ret에 들어간다.
if (ret >= 0)
{
dump(data, ret);
}
// printf("payload_len=%d\n", ret);// ret는 패킷의 길이
// fputc('\n', stdout);
return id;
}
print_pkt 함수는 아까 callback의 nfa를 받고 패킷의 정보를 출력한다. 그러나 우리가 원하는 것을 출력할 것이기에 출력하는 부분은 없앴다. ph는 packet 헤더 받는 부분인듯. 거기서 id를 받아오고 hwph에는 몸통 부분을 받아오나..?
어쨌든 밑에 ret 부분이 중요한데 패킷에 대한 시작 위치를 data에 넣고 그니까 결국 data에 패킷이 들어가 있고, ret에는 그 길이가 들어가서
dump를 통해서 data, ret를 전달해주고 실행
id를 아까 callback 함수에 id 자리에 반환
rv는 recv의 약자인 것 같은데 recv
참고로 실행할 때는 sudo로 호출해야 함
void dump(unsigned char* buf, int size) { // data와 ret를 확인하는 함수.패킷 데이터가 출력되는 것을 볼 수 있음. ip 헤더부터 시작
struct libnet_ipv4_hdr *ipv4 = (struct libnet_ipv4_hdr *) (buf); //libent 사용
struct libnet_tcp_hdr *tcp = (struct libnet_tcp_hdr *) (buf + sizeof(*ipv4));
uint32_t sip = htonl(ipv4->ip_src.s_addr);
uint32_t dip = htonl(ipv4->ip_dst.s_addr);
uint8_t ip1, ip2, ip3, ip4;
printf("===========================\n");
printf("Source IP: ");
ip1 = (sip & 0xff000000) >> 24;
ip2 = (sip & 0x00ff0000) >> 16;
ip3 = (sip & 0x0000ff00) >> 8;
ip4 = (sip & 0x000000ff);
printf("%d.%d.%d.%d\n",ip1, ip2, ip3, ip4);
printf("Destination IP: ");
ip1 = (dip & 0xff000000) >> 24;
ip2 = (dip & 0x00ff0000) >> 16;
ip3 = (dip & 0x0000ff00) >> 8;
ip4 = (dip & 0x000000ff);
printf("%d.%d.%d.%d\n",ip1, ip2, ip3, ip4);
if(ipv4->ip_p == 0x06)
{
printf("tcp\n");
is_tcp = 1;
printf("Source Port: ");
printf("%d\n",ntohs(tcp->th_sport));
printf("Destination Port: ");
printf("%d\n",ntohs(tcp->th_dport));
}
printf("===========================\n");
/*
int i;
for (i = 0; i < size; i++) {
if (i != 0 && i % 16 == 0)
printf("\n");
printf("%02X ", buf[i]);
}*/
printf("\n");
}
dump가 내가 만든 부분이고 libnet.h 헤더파일로 구조체를 받아서 패킷의 형식에 따른 의미를 분류하기로 했다.
함수 내용은 그 과정이고
tcp를 drop하는 것이 과제였으므로 tcp인지 알 수 있는 부분인 ipv4->ip_p가 0x6이면
그러니까 3계층의 네트워크 계층에서 ip_p 부분이 0x06이면 다음 계층 트랜스포트에서 tcp를 사용한다는 뜻이기에(pcap 글 참고)
is_tcp를 1로 바꾼다.
다시 callback로 돌아가서
is_tcp가 1로 바뀌어 있으면 DROP을 하고
아니면 ACCEPT
우선 네트워크 프로젝트에서 할 일은 (모두 h, cpp로 나누어서 정리해야 함)
nf를 통해서 들어온 tcp패킷의 정보를 받고. (data가 받을 듯)
프록시서버가 아니라면 (내부 단말기)
그 패킷의 정보를 토대로 (이미 들어온 패킷은 DROP)header로 fake_packet을 만들고 (retransmission, ip 등을 수정)
pcap을 통해서 패킷을 proxy server로 보낸다.
프록시 서버라면
들어온 패킷의 헤더 부분을 제거하고 DROP
나머지는 pcap으로 재전송 해야할듯..