2012年1月18日 星期三

自定義iptables/netfilter的目標模塊

 

自定義iptables/netfilter的目標模塊

 
本文檔的Copyleft歸yfydz所有,使用GPL發布,可以自由拷貝,轉載,轉載時請保持文檔的完整性,嚴禁用於任何商業用途。
msn: yfydz_no1@hotmail.com
來源: http://yfydz.cublog.cn

1. 前言
Linux中的netfilter提供了一個防火牆框架,具有很好的擴展性,除了自帶的模塊之外,用戶可以根據自己的需求定義新的防火牆模塊加入其中,而編程過程也不是很複雜,只要依葫蘆畫瓢即可,可在原來的類似功能的模塊基礎上修改即可,甚至對各函數是如何調用,一些內部結構是如何定義的都不用詳細了解,本文即介紹如何編寫自定義的目標模塊。
 
目標(target)是防火牆規則策略中的結果部分,定義對數據包要進行如何處理,如接受、丟棄、修改、繼續等。在具體實現時分為兩部分,內核部分和用戶空間部分:內核中是以netfilter的擴展模塊實現,定義該模塊並將其掛接到netfilter的目標鍊錶中後,在內核進行目標檢測時自動根據目標名稱查找該模塊而調用相應的目標函數,完成真正的目標功能;在用戶空間,目標模塊是作為iptables的一個擴展動態庫來實現,只是一個用戶接口,不完成實際目標功能,完成接收用戶輸入並將目標數據結構傳遞到內核的功能,該庫的名稱有限制,必須為libipt_xxx.so,其中“xxx”是該目標的名字,為區別於匹配,
通常目標的名稱一般都是用大寫。

2. 內核模塊
目標在2.4內核和在2.6內核中的函數參數略有區別,兩者不兼容,但只要簡單修改後即可相互移植,主體部分不需要改變,本文以2.6內核的模塊為例。
 
為方便說明,還是通過舉例來進行說明,要實現的目標是修改IP頭中的ID字段,在實際使用中這個功能是沒什麼實際意義的,只是用來舉例說明。
 
在內核中已經自帶了net/ipv4/netfilter/ipt_ID.c模塊用來修改IP頭中的ID字段,我們可以以此模塊為基礎來進行修改。
 
首先要定義要進行目標的數據結構,用來描述目標條件,以ipt_ID.h頭文件為基礎修改為:

/* include/linux/netfilter_ipv4/ipt_ID.h */ 
#ifndef _IPT_ID_H_target 
#define _IPT_ID_H_target
struct ipt_id_target_info { 
 /* network order. */ 
 u_int16_t id; // id是16位的數
};
#endif /*_IPT_ID_H_target*/
 
然後是定義內核模塊處理,最主要的是定義一個ipt_target目標結構,該結構在include/linux/netfilter_ipv4/ip_tables.h中定義:
 
/* Registration hooks for targets. */ 
struct ipt_target 

 struct list_head list;
 const char name[IPT_FUNCTION_MAXNAMELEN];
 /* Called when user tries to insert an entry of this type: 
           hook_mask is a bitmask of hooks from which it can be 
           called. */ 
 /* Should return true or false. */ 
 int (*checkentry)(const char *tablename, 
     const struct ipt_entry *e, 
     void *targinfo, 
     unsigned int targinfosize, 
     unsigned int hook_mask);
 /* Called when entry of this type deleted. */ 
 void (*destroy)(void *targinfo, unsigned int targinfosize);
 /* Returns verdict. Argument order changed since 2.4, as this 
           must now handle non-linear skbs, using skb_copy_bits and 
           skb_ip_make_writable. */ 
 unsigned int (*target)(struct sk_buff **pskb, 
          const struct net_device *in, 
          const struct net_device *out, 
          unsigned int hooknum, 
          const void *targinfo, 
          void *userdata);
 /* Set this to THIS_MODULE. */ 
 struct module *me; 
};

該結構中有以下幾個參數:
struct list_head list:用來掛接到目標鍊錶,必須初始化為{NULL, NULL};
name:該目標名稱的名稱,必須是唯一的;
checkentry函數:用於對用戶層傳入的 ​​數據進行合法性檢查,如目標數據長度是否正確,是否是在正確的表中使用等;
destroy函數:用於釋放該目標中動態分配的資源,在規則刪除時會調用;
target函數:該函數是最主要函數,完成對數據包的策略處理,包括對包中的數據進行修改,函數返回結果可能是NF_ACCEPT(接受)/NF_DROP(丟棄)/NF_STOLEN(偷竊,指該包處理由該目標接管,不再由系統網絡棧處理)/IPT_CONTINUE(繼續,繼續按後面的規則對該包進行檢查)等;
struct module *me:指向模塊本身。
 
本例中的目標結構定義如下:
static struct ipt_target ipt_id_reg = { 
 .name = "ID", 
 .target = target, 
 .checkentry = checkentry, 
 .me = THIS_MODULE, 
};
 
target函數是目標的核心函數,返回1表示數據符合目標條件,0表示不目標,函數定義為:
 unsigned int (*target)(struct sk_buff **pskb, 
          const struct net_device *in, 
          const struct net_device *out, 
          unsigned int hooknum, 
          const void *targinfo, 
          void *userdata); 
在target函數參數為:
skb:數據包
in:數據進入的網卡
out:數據發出的網卡
hooknum:hook號,取值為:NF_IP_PRE_ROUTING/NF_IP_LOCAL_IN/NF_IP_FORWARD /
NF_IP_LOCAL_OUT/NF_IP_POST_ROUTING之一
targetinfo:目標條件信息的指針
offset:碎片數據偏移量
userdata:特殊專用數據,目前內核中基本沒有用到,通常都是NULL;
 
 
check函數對數據進行檢查,返回1表示數據合法,0表示非法,函數定義為:
 int (*checkentry)(const char *tablename, 
     const struct ipt_entry *e, 
     void *targinfo, 
     unsigned int targinfosize, 
     unsigned int hook_mask) ;
tablename:表名,如“filter”,“nat”,“mangle”等,可用來限制目標只能在指定的表中處理;
struct ipt_entry *e:指向規則的指針;
targetinfo:用戶空間傳入的 ​​目標條件信息的指針;
targetinfosize:用戶空間傳入目標條件信息的長度;
hook_mask:表示掛接點(PREROUTING/INPUT/FORWARD/OUTPUT/POSTROUTING)的掩碼,可用來限制目標只能在指定的掛接點中處理;
 
宏IPT_ALIGN用來得到實際目標結構的實際大小。
 
本例中需要檢查目標數據長度是否正確,表名是否是“mangle”,因為需要對數據進行修改,所以一般要在mangle表中進行處理,其實也可以不限制;
 
destroy函數釋放該目標中動態分配的資源,無返回值,函數定義為:
 void (*destroy)(void *targetinfo, unsigned int targetinfosize); 
函數參數同check()函數說明,在本例中,並沒有分配相關資源,所以沒定義此函數。
 
最後在模塊初始化函數中要調用ipt_register_target()函數將一個ipt_target目標結構掛接到系統目標鍊錶中,在模塊的結束函數中調用ipt_unregister_target()函數將目標結構從目標鍊錶中去除。
 
修改net/ipv4/netfilter/Makefile和Kconfig文件,加入關於ipt_ID相關內容即可在編譯內核中自動編譯,或者單獨直接將其編譯為模塊插入內核。
 
ipt_ID.c代碼如下:
/* This is a module which is used for setting the ID field of a packet. 
 * based on ipt_ID.c 
 * in fact, it's useless. 
 */
#include  
#include  
#include  
#include
#include  
#include
MODULE_LICENSE("GPL"); 
MODULE_AUTHOR("yfydz< yfydz_no1@hotmail.com >"); 
MODULE_DESCRIPTION("iptables ID mangling module");
 
static unsigned int 
target(struct sk_buff **pskb, 
       const struct net_device *in, 
       const struct net_device *out, 
       unsigned int hooknum, 
       const void *targinfo, 
       void *userinfo) 

 const struct ipt_id_target_info *idinfo = targinfo;
//如果當前包的ID值和指定值不同,將指定值賦值給IP頭中的ID字段
 if ((*pskb)->nh.iph->id != idinfo->id) { 
  u_int16_t diffs[ 2];
  if (!skb_ip_make_writable(pskb, sizeof(struct iphdr))) 
   return NF_DROP;
  diffs[0] = htons((*pskb)->nh.iph->id) ^ 0xFFFF; 
  (*pskb)->nh.iph->id 
   = idinfo->id; 
  diffs[1] = htons((* pskb)->nh.iph->id); 
//由於IP包數據進行了修改,需要重新計算IP頭中的校驗和
  (*pskb)->nh.iph->check 
   = csum_fold(csum_partial(( char *)diffs, 
       sizeof(diffs), 
       (*pskb)->nh.iph->check 
       ^0xFFFF)); 
  (*pskb)->nfcache |= NFC_ALTERED; 
 } 
//返回IPT_CONTINUE,表示繼續按下一條規則處理數據包
 return IPT_CONTINUE; 
}
 
static int 
checkentry(const char *tablename, 
    const struct ipt_entry *e, 
           void *targinfo, 
           unsigned int targinfosize, 
           unsigned int hook_mask) 

 const u_int8_t id = ((struct ipt_id_target_info *)targinfo)->id; 
//檢查用戶傳入數據的長度是否正確
 if (targinfosize != IPT_ALIGN(sizeof(struct ipt_id_target_info))) { 
  printk(KERN_WARNING "ID: targinfosize %u != %Zu\n", 
         targinfosize, 
         IPT_ALIGN(sizeof(struct ipt_id_target_info))); 
  return 0; 
 } 
//判斷是否在mangle表中
 if (strcmp(tablename, "mangle") != 0) { 
  printk(KERN_WARNING "ID: can only be called from \"mangle\" table, not
\"%s\"\n", tablename); 
  return 0; 
 }
 return 1; 
}
 
static struct ipt_target ipt_id_reg = { 
 .name = "ID", 
 .target = target, 
 .checkentry = checkentry, 
 .me = THIS_MODULE, 
};
 
static int __init init(void) 

 return ipt_register_target(&ipt_id_reg); 
}
 
static void __exit fini(void) 

 ipt_unregister_target(&ipt_id_reg); 
}
 
module_init(init); 
module_exit(fini);
 
如果對包中的數據進行了修改,需要修改數據頭相應的校驗和,如IP頭校驗和和TCP/UDP頭校驗和,所以可能會需要調用如csum_partial(),tcp_v4_check(),csum_tcpudp_magic (),ip_s​​end_check(iph)等函數計算校驗和,這些函數的使用方法可參考內核中的代碼實例。
 
有些目標對包的操作比較複雜,如REJECT,MIRROR等,需要構造回應包發出,所以一般目標要比匹配要復雜一些。

3. iptables用戶層目標模塊
iptables中的擴展目標模塊是以動態庫方式處理,在命令行中用“-j xxx”來使iptables調用相應的libipt_xxx.so動態庫,擴展的目標代碼通常在iptables-/extension目錄下,編譯好的動態庫缺省放在/usr/local/lib/iptables目錄下。
 
目標動態庫的作用用於解析用戶輸入的目標信息,顯示目標信息等功能。
 
寫好libipt_xxx.c程序後放到iptables-/extension目錄下,修改該目錄下的Makefile文件,將xxx添加到擴展表中,make就能自動將其編譯為動態庫。
 
對於目標,最重要的數據結構就是struct iptables_target結構,擴展的目標程序就是要定義一個這個結構並將其掛接到iptables目標鍊錶中,該結構定義如下:
 
struct iptables_target 

 struct iptables_target *next;
 ipt_chainlabel name;
 const char *version;
 /* Size of target data. */ 
 size_t size;
 /* Size of target data relevent for userspace comparison purposes */ 
 size_t userspacesize;
 /* Function which prints out usage message. */ 
 void (*help)(void);
 /* Initialize the target. */ 
 void (*init)(struct ipt_entry_target *t, unsigned int *nfcache);
 /* Function which parses command options; returns true if it 
           ate an option */ 
 int (*parse)(int c, char **argv, int invert, unsigned int *flags, 
       const struct ipt_entry *entry, 
       struct ipt_entry_target **target );
 /* Final check; exit if not ok. */ 
 void (*final_check)(unsigned int flags);
 /* Prints out the target iff non-NULL: put space at end */ 
 void (*print)(const struct ipt_ip *ip, 
        const struct ipt_entry_target *target, int numeric);
 /* Saves the targinfo in parsable form to stdout. */ 
 void (*save)(const struct ipt_ip *ip, 
       const struct ipt_entry_target *target);
 /* Pointer to list of extra command-line options */ 
 struct option *extra_opts;
 /* Ignore these men behind the curtain: */ 
 unsigned int option_offset; 
 struct ipt_entry_target *t; 
 unsigned int tflags; 
 unsigned int used; 
#ifdef NO_SHARED_LIBS 
 unsigned int loaded; /* simulate loading so options are merged properly */ 
#endif 
};
 
struct iptables_target結構參數說明如下:
next:目標鍊錶的下一個,目標鍊錶是一個單向鍊錶;
name:目標的名稱,必須是唯一的;
version:iptables的版本;
size:目標結構的數據長度;
userspacesize :用於目標部分的數據長度,通常此值等於size,但某些情況 ​​可能會小於size;
help函數:打印幫助信息,當"-j xxx -h"時調用;
init函數:初始化函數,可對目標結構賦初值;
parse函數:解析用戶輸入參數,這是最主要的處理函數;
final_check函數:對用戶數據進行最後的檢查;
print函數:打印目標信息,iptables -L時調用
save函數:保存當前iptables規則時打印目標格式,被iptables-save程序調用;
extra_opts:選項信息,選項格式是標準的UNIX選項格式,通過getopt函數識別;
option_offset:選項偏移;
t:指向iptables規則;
tflags:規則相關標誌
used:模塊使用計數;
 
本例中,用戶輸入的參數是新的ID值,格式為:
“-j ID --set-id id_value ” id_value為id的實際數值。
 
libipt_id.c函數就是填寫struct iptables_target結構,然後定義動態庫初始化函數_init()將該結構掛接到iptables的選項鍊表中。程序在libipt_id.c的基礎上修改,程序比較簡單,直接將代碼列出,相關說明在註釋中:
 
/* libipt_id.c */ 
/* Shared library add-on to iptables to add ID target support. */ 
#include  
#include  
#include  
#include
#include  
#include  
#include
struct idinfo { 
 struct ipt_entry_target t; 
 struct ipt_id_target_info id; 
};

/* Function which prints out usage message. */ 
static void 
help(void) 

 unsigned int i;
 printf( 
"ID target v%s options:\n" 
" --set-id value Set ID of IP headere\n", 
IPTABLES_VERSION);
}
 
// opts結構第一個參數為選項名稱,
//第二個參數為1表示選項名稱後還帶參數,為0表示選項名稱後不帶參數
//第3個參數是標誌,表示返回數據的格式,一般都設為0 
//第4個參數表示該選項的索引值
static struct option opts[] = { 
 { "set-id", 1, 0, '1' }, 
 { 0 } 
} ;
/* Initialize the target. */ 
static void 
init(struct ipt_entry_target *t, unsigned int *nfcache) 

//空函數,不需要預處理
}
 
static void 
parse_id(const unsigned char *s, struct ipt_id_target_info *info) 

 unsigned int i, id;
 if (string_to_number(s, 0, 255, &id) != -1) { 
//注意要將主機序數據轉換為網絡序
      info->id = htons((u_int16_t )id); 
      return; 
 } 
 exit_error(PARAMETER_PROBLEM, "Bad ID value `%s'", s); 
}
 
/* Function which parses command options; returns true if it 
   ate an option */ 
static int 
parse(int c, char **argv, int invert, unsigned int *flags, 
      const struct ipt_entry *entry, 
      struct ipt_entry_target **target) 

 struct ipt_id_target_info *idinfo 
  = (struct ipt_id_target_info *)(*target)->data;
 switch (c) { 
 case '1': 
  if (*flags) 
   exit_error(PARAMETER_PROBLEM, 
              "ID target: Cant specify --set-id twice"); 
  parse_id(optarg, idinfo); 
  *flags = 1; 
  break;
 default: 
  return 0; 
 }
 return 1; 
}
 
static void 
final_check(unsigned int flags) 

 if (!flags) 
  exit_error(PARAMETER_PROBLEM, 
             "ID target: Parameter --set-id is required"); 
}
 
static void 
print_id(u_int8_t id, int numeric) 

 unsigned int i;
 printf("0x%x ", ntohs(id)); 
}
 
/* Prints out the targinfo. */ 
static void 
print(const struct ipt_ip *ip, 
      const struct ipt_entry_target *target, 
      int numeric) 

 const struct ipt_id_target_info *idinfo = 
  (const struct ipt_id_target_info *)target->data; 
 printf("ID set "); 
 print_id(idinfo->id, numeric); 
}
 
/* Saves the union ipt_targinfo in parsable form to stdout. */ 
static void 
save(const struct ipt_ip *ip, const struct ipt_entry_target *target) 

 const struct ipt_id_target_info *idinfo = 
  (const struct ipt_id_target_info *)target->data;
 printf("--set-id 0x%x ", ntohs(idinfo->id)); 
}
 
static 
struct iptables_target id 
= { NULL, 
    "ID", 
    IPTABLES_VERSION, 
    IPT_ALIGN(sizeof(struct ipt_id_target_info)), 
    IPT_ALIGN(sizeof(struct ipt_id_target_info)), 
    &help, 
    &init, 
    &parse, 
    &final_check, 
    &print, 
    &save, 
    opts 
};
 
void _init(void) 

 register_target(&id); 
}
 
4. 結論
netfilter/iptables可以很方便地擴展新的目標模塊,只需要按指定的方式編寫代碼就可以,讓開發者將注意力集中在功能的具體實現上,而不用再考慮其他因素,在具體實現時可以以現成的目標模塊為基礎進行修改即可,甚至不需要更仔細了解內部結構的定義就可以完成編碼,是一個程序模塊化的很優秀的實現例子。在netfilter官 ​​方網站( www.netfilter.org )上提供patch-o-matic程序包,其中包含了許多愛好者編寫的未併入Linux官方內核中的匹配和目標模塊。

How to use simple speedtest in RaspberryPi CLI

  pi@ChunchaiRPI2:/tmp $  wget -O speedtest-cli https://raw.githubusercontent.com/sivel/speedtest-cli/master/speedtest.py --2023-06-26 10:4...