读书人

Linux个人防火墙JAVA版兑现(Netfilter

发布时间: 2012-08-22 09:50:35 作者: rapoo

Linux个人防火墙JAVA版实现(Netfilter+Netlink+Multi-Thre) in 2009
@Author: cjcj cj.yangjun@gmail.com <c-j.iteye.com>


程序demo:
毕业设计会放到 http://code.google.com上,有兴趣一起完善的兄弟联系我哈!!

NAT实现

进度:
5.13
潦草地完成NAT模块,暂时收工。





完成netlink的通讯,以及修改pool池数据结构的bug.完成状态存储,查询匹配功能。



5.7凌晨
完成内核态初步钩子函数。匹配ip,tcp,udp信息


内核的数据结构很讲究共性和特性。这个我还没感知到。
目前暂时完成用于存储的数据结构和固定大小队列,其实应该用hashtable的。结构体如下:

struct object_table{__be32saddr;__be32daddr;__be16source;__be16dest;int status;//static int MAXNUM=0;struct object_table *next;struct object_table *prv;//为了便于从尾部查询}

概念:
流程:

eth0(public ip 10.10.10.1):
DNAT(PRE_ROUTING):
接受外网的数据包。saddr(202.202.202.2) daddr(10.10.10.1) 需要查询匹配(ip+port),查询202.202.202.2+port。
匹配成功,则根据查询结果修改daddr为192.168.0.2+port(查询从尾部开始)
匹配不成功,则无须更改或者丢弃。
接受内网的数据包。saddr(192.168.0.2) daddr(202.202.202.2)
SNAT(POST_ROUTING):
发送数据包给外网。saddr(192.168.0.2) daddr(202.202.202.2)需要存储状态,存储192.168.0.2+port,202.202.202.2+port,并修改saddr为10.10.10.1。(为了提高效率,不用查询后插入)
发送数据包给内网。saddr(202.202.202.2) daddr(192.168.0.2)

eth1(private ip 192.168.0.1):(所有的数据包都转发给eht0处理)
eth3(ip 192.168.0.2);
eth4(ip 202.202.202.2);




5个挂接点以下内核代码版本2.6.251.1 PREROTING/* net/ipv4/ip_input.c */int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev){......return NF_HOOK(PF_INET, NF_INET_PRE_ROUTING, skb, dev, NULL,       ip_rcv_finish);......}1.2 INPUT/* net/ipv4/ip_input.c */int ip_local_deliver(struct sk_buff *skb){......return NF_HOOK(PF_INET, NF_IP_LOCAL_IN, skb, skb->dev, NULL,         ip_local_deliver_finish);}1.3 FORWARD/* net/ipv4/ip_forward.c */int ip_forward(struct sk_buff *skb){......return NF_HOOK(PF_INET, NF_IP_FORWARD, skb, skb->dev, rt->u.dst.dev,         ip_forward_finish);......}1.4 OUTPUT/* net/ipv4/ip_output.c */int ip_build_and_send_pkt(struct sk_buff *skb, struct sock *sk,  __be32 saddr, __be32 daddr, struct ip_options *opt){......return NF_HOOK(PF_INET, NF_IP_LOCAL_OUT, skb, NULL, rt->u.dst.dev,         dst_output);}int __ip_local_out(struct sk_buff *skb){struct iphdr *iph = ip_hdr(skb);iph->tot_len = htons(skb->len);ip_send_check(iph);return nf_hook(PF_INET, NF_INET_LOCAL_OUT, skb, NULL, skb->dst->dev,dst_output);}1.5 POSTROUTING/* net/ipv4/ip_output.c */int ip_mc_output(struct sk_buff *skb){ ......return NF_HOOK_COND(PF_INET, NF_INET_POST_ROUTING, skb, NULL, skb->dev,ip_finish_output,!(IPCB(skb)->flags & IPSKB_REROUTED));}int ip_output(struct sk_buff *skb){......return NF_HOOK_COND(PF_INET, NF_INET_POST_ROUTING, skb, NULL, dev,ip_finish_output,!(IPCB(skb)->flags & IPSKB_REROUTED));......}



private Tank tank; public synchronized void putTank(Tank tank){ //Dont bother to check whether it has room. this.tank=tank; notifyAll(); } public synchronized Tank getTank(){ //Check whether there's tank to consume while(tank==null){ //No tank yet, let's wait. try{ wait(); }catch(InterruptedException e){} } Tank retValue=tank. tank=null; //Clear tank. return retValue; }}


ok.

连接状态信息获取出来了,用的另一种思路,不依附以前的netlink,而直接通过系统的proc文件系统获取的信息,这样不要考虑同步异步问题,也提高了多线程的效率。



不过一下午都纠结在如何分解这些字符串中,但后来还是省去了这种繁琐的分解上面。






另辟溪路:
谢谢seen大哥给出的另一条抓包的路,确实也不用考虑阻塞问题了,我原以为netlink的效率会很高。
如果nf_conntrack内核模块与user space只有唯一的"proc"通讯的话,那只能用"cat方式“了。给后期的nat实现带来了一定的影响。


连接跟踪实现:


貌似现在的思路越来越明朗了:

关于链接跟踪

1,通过"proc"与nf_conntrack内核模块通讯,获取连接信息


2,通过之前自己已实现ip包过滤模块的hook,来获得ip头,tcp,udp头信息并通过netlink获取连接信息

类似“ipv4 2 tcp 6 5 TIME_WAIT src=192.168.0.25 dst=221.238.249.178 sport=39027 dport=80 packets=6 bytes=1119 src=221.238.249.178 dst=192.168.0.25 sport=80 dport=39027 packets=5 bytes=2474 [ASSURED] mark=0 secmark=0 use=1"

只是里面的bytes mark secmark use TIME_WAIT 这几个信息不知道是从哪来的???


_________________________________________________
发现不是单播 和多播的问题

问题出再多线程建立多个sock时候发生阻塞,虽然局部变量sock在有时候能够成功,但在多线程同时创建的时候就发生错误了。
没办法,只能共享一个sock链接和绑定了,但动态获取ip数据包的时候也很容易和添加禁止ip发生阻塞,内核模块只要稍微没做的好,用户态这么的线程接收函数就会出现“死锁”现象,所以确定需要解决的重点在:不管网络层是否有网络数据包,内核模块都需要即使返回消息给用户态,这样才不会让用户态无限等下去....这是今天解决的问题。
并分享下今天的成果:



________________________________________________________
单播与多播
虽然前几天解决了多线程发送消息的问题,没有发生错误,但是感觉效果不是很好,我添加了端口监听的小功能,发现和监听IP信息包同时启动共用同一协议的NL连接时的反应很慢,甚至丢失数据。
因为在外面,今天晚上再去试试多播异步传送。

int netlink_unicast(struct sock *ssk, struct sk_buff *skb, u32 pid, int nonblock); 


data指向要被发送的 netlink信息,pid是接收进程的pid号。这里使用NLPID公式1。Nonblock表示这个API在接收缓冲不可用或者立即返回失败时是不是 被阻塞。

我们也可以使用多播来发送信息。下面的函数将发送一个消息到一个配置了特殊pid和多播组的进程。


代码:
void netlink_broadcast(struct sock *ssk, struct sk_buff *skb, u32 pid, u32 group, int allocation); 


其中group是所有接收的多播组的标志号。Allocation是内核内存分配类型。如果是再中断上下文环境,有GFP_ATOMIC,其他的有GFP_KERNEL。这取决于内核需要分配一个内存块还是分配多个缓冲用来克隆这个多播信息。


______________________________________
解决多线程sock通讯问题

因为对netlink没有做深入了解,所以本以为多线程创建多个sock链接就可以解决同步问题,结果出现了error..
而发生错误的原因是多线程创建sock后,bind()操作失败,换句话说,我需要保证做过滤ip操作和获取Ip信息包操作不能在底层发生冲突。
于是乎,有想到再kernel_module多创建个netlink链接,结果还是以失败告终。在这简单介绍下创建nl操作
nlfd = netlink_kernel_create(&init_net,NL_FW,0, kernel_receive,NULL,THIS_MODULE);

2.6.25的原型是:
extern struct sock *netlink_kernel_create(struct net *net,
int unit,unsigned int groups,
void (*input)(struct sk_buff *skb),
struct mutex *cb_mutex,
struct module *module);


这里只关注 NL_FW 是nl的自定义标示,kernel_receive是内核自动绑定接收消息函数,最后个参数是指定模块

于是又去查,发现每次单播消息是通过getpid()这个进程ip作为通讯的标示符,于是如果对不同操作定义不同的唯一的标示符,试了试,果然可行。粗略代码如下:

User Space:
  local.nl_family = AF_NETLINK;//设置环境  local.nl_pid = NL_IPINFO;//通讯唯一表示的id  local.nl_groups = 0;//是否指定多播,我理解为可指定进程组   //用于把一个打开的 netlink socket 与 netlink 源 socket 地址绑定在一起  if(bind(skfd, (struct sockaddr*)&local, sizeof(local)) != 0)    {      printf("bind() error in getIpinfo\n");      return 0;    }  memset(&kpeer, 0, sizeof(kpeer));  kpeer.nl_family = AF_NETLINK;  kpeer.nl_pid = 0;  //0表示内核进程  kpeer.nl_groups = 0; message.hdr.nlmsg_pid = local.nl_pid; //发送消息包给内核  sendto(skfd, &message, message.hdr.nlmsg_len, 0, (struct sockaddr*)&kpeer, sizeof(kpeer));

Kernel Space:

 else if(nlh->nlmsg_type == FW_IP_INFO)       {write_lock_bh(&user_proc.lock);user_proc.pid = nlh->nlmsg_pid;write_unlock_bh(&user_proc.lock);      }


______________________________________________________

static void del_ip_list(struct proc *proc)
{
struct proc_list *temp=NULL;
struct proc_list *head=proc->head;
printk("in del_ip_list\n");
while(head!=NULL){
temp=head;
head=head->next;
kfree(temp);
printk("del one ip\n");
}
proc->head=NULL;
}

这个置空操作很诡异,弄了我一下午的时间。郁闷!让我机器重启了很多次了



linux 防火墙 简单来说就是利用netfilter的那套框架或者说HOOK/

价值:看到以前的防火墙大多是在2.4版本下的,而2.6.24之后发生大的变化了。
研究思路:

(1)、在Linux内核2.6平台上,首先实现内核模块的编写和加载,学会Netfilter的实现框架,并自己用C语言实现包过滤、数据追踪等功能;
(2)设计一套自己的框架来创建和加载用户规则;接着实现用JAVA编写的UI(界面)并调用底层的C语言用户库,并实现用户态与内核态双方的即时通讯;
(3)完善系统的整体功能和构架。并尽量做到设计上的优化、操作上的简便和使用上的高效。
方案:
用Java Swing做UI,用Netfilter框架注册自己写的C语言规则,用JNI实现用户空间与系统空间的交互,用模块编程把系统加载到Linux操作系统中。

——————————————————————————————————————————
解决2.6内核模块加载问题
——————————————————————————————————————————
解决端口包过滤,包含目的,源端口
——————————————————————————————————————————
解决IP包过滤问题
unsigned int ---------> __u32;
__u32 --------->__bitwise __be32;
__be32 ---------> saddr;
所以:saddr 实际上是unsigned int类型
输出: printk("Packet from %u\n",iph->saddr);
解决unsigned int * / unsigned char *的比较问题
解决IP地址与unsigned int *转换问题
http://libkf701.googlecode.com/svn/trunk/
**
**
——————————————————————————————————————————
解决JNI问题与Linux内核交互问题:
In static block, we are loading a DLL named TestDll.dll which should be placed in <WINDIR>/SYSTEM32 directory. I am not saying you cannot put this DLL in other directories, you can, but then you will have to register that path in JVM. Here you can call another method as well, instead of loadLibrary().
Collapse Copy Code

System.load(“c://winnt//system32//TestDll.dll”);
I think load() method is better as far as usage is concerned because you can easily place your DLL in any directory.
——————————————————————————————————————————
解决用户态与内核态交涉问题 ,以后再整理一篇博客出来
基本原理:

iptable好像走的是
sockfd=socket(AF_INET, SOCK_RAW, IPPROTO_RAW)
setsockopt(sockfd, IPPROTO_IP, ......)
ip_setsockopt 如果发现不在范围的选项,可能会调用nf_setsockopt
而后者就会检查nf_register_sockopt注册的了
如果level 不是 SOL_SOCKET这个的话,应该就调用sock->ops->setsockopt()吧,这个地方没找到在初始化。
按照版主的说法,应该在这个里面实现选项的查找,然后没有查找到的话,调用nf_setsockopt
设计基本定下用netlink实现用户态到内核态的即时通讯了.
今天在公司基本看了一天的netlink,因为没环境,所以得晚上回来弄,后来发现2.6.24内核版本后做了改动,主要还是再create上面,把以前用while控制的读取消息队列,该成了自动绑定```
刚弄完内核模块的任务,明天看看用户态的实现,今天也是晚上睡了会,所以很晚才弄,接触过这么久的linux,这次才真正感觉弄了点实质的东西,呵呵```` 毕业设计估计老师望着我急,我也只能望着他干瞪眼了``
我的小乖乖开始通讯了:ICMP网络层抓截取数据包实现如下



关于自旋锁:
自旋锁最多只能被一个可执行线程持有。自旋锁不会引起调用者睡眠,如果一个执行线程试图获得一个已经被持有的自旋锁,那么线程就会一直进行忙循环,一直等待下去,在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。
由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁。
信号量和读写信号量适合于保持时间较长的情况,它们会导致调用者睡眠,因此只能在进 程上下文使用(_trylock的变种能够在中断上下文使用);而自旋锁适合于保持时间非常短的情况,因为一个被争用的自旋锁使得请求它的线程在等待重新 可用时自旋,特别浪费处理时间,这是自旋锁的要害之处,所以自旋锁不应该被长时间持有。
**
**
关于与内核通讯小结:
方式:bootloader,sysfs,sysctl,系统调用,netlink,procfs,seq_file,debugfs,relayfs
**
bootloader
内核启动参数方式是单向的,即只能向内核传递,而不能从内核获取,并且它是启动时候为内核传递参数,正因为这个存在,用户态才可以在系统启动时候为操作系统设置环境了.
这种方式可以加载到新内核中了.
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/string.h>
#define MAX_SIZE 5
static int setup_example_int;
static int setup_example_int_array[MAX_SIZE];
static char setup_example_string[16];
static int __init parse_int(char * s)//参数处理函数
{
int ret;
ret = get_option(&s, &setup_example_int);//内核为整数参数值的分析提供了函数 get_option 和 get_options
if (ret == 1) {
printk("setup_example_int=%d\n", setup_example_int);
}
return 1;
}
static int __init parse_string(char *s)
{
if (strlen(s) > 15) {
printk("Too long setup_example_string parameter, \n");
printk("maximum length is less than or equal to 15\n");
}
else {
memcpy(setup_example_string, s, strlen(s) + 1);
printk("setup_example_string=%s\n", setup_example_string);
}
return 1;
}
__setup("setup_example_string",setup_example_string);


Sysfs:

文字丢失








#include <linux/config.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/stat.h>
static int my_int_para = 0;
static char * my_string_para = "Hello, World";
static int __init exam_module_init(void)
{
printk("my_int = %d\n", my_int_para);
printk("my_string = '%s'\n", my_string_para);
return 0;
}
static void __exit exam_module_exit(void)
{
printk("my_int = %d\n", my_int_para);
printk("my_string = '%s'\n", my_string_para);
}
module_init(exam_module_init);
module_exit(exam_module_exit);
module_param(my_int_para, int, 0);//通过宏module_param可以挂到sysfs里面
MODULE_PARM_DESC(my_int_para, "An invisible int under sysfs");
module_param(my_string_para, charp, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
MODULE_PARM_DESC(my_string_para, "An visible string under sysfs");
MODULE_LICENSE("GPL");
**


sysctl:
Sysctl是一种用户应用来设置和获得运行时内核的配置参数的一种有效方式,通过这种方式,用户应用可以在内核运行的任何时刻来改变内核的配置参数,也可以在任何时候获得内核的配置参数,通常,内核的这些配置参数也出现在proc文件系统的/proc/sys目录下,用户应用可以直接通过这个目录下的文件来实现内核配置的读写操作,例如,用户可以通过
Cat /proc/sys/net/ipv4/ip_forward
来得知内核IP层是否允许转发IP包,用户可以通过
echo 1 > /proc/sys/net/ipv4/ip_forward
**
系统调用(2.6.25):
1. linux/kernel/sys.c
在该文件里增加了 一个自己的函数:

asmlinkage void sys_hello(void)
{
printk("Hello world!\n");
return ;
}

2. linux/include/asm/unistd_32.h
添加系统调用号: #define __NR_hello 325
并且把 #define __NR_syscalls 325 改为 #define __NR_syscalls 326

3. linux/arch/x86/kernel/syscall_table_32.S
添加指向自己增加函数的指针
.long sys_hello

4. linux/syscalls.h
添加自己的系统调用的函数声明:
asmlinkage void sys_hello (void);

接下来编译内核:

动态截取系统调用:http://www.ibm.com/developerworks/cn/linux/l-knldebug/index.html?ca=dwcn-newsletter-linux
netlink
是一种双向的数据交换方式,它使用起来非常简单高效,特别是它的广播特性在一些应用中非常方便。它是所有这些用户态与内核态数据交换方式中最有效的最强大的方式。
seq_file是单向的,即只能向内核传递,而不能从内核获取,而另外三种方式均可以进行双向数据交换,即既可以从用户应用传递给内核,又可以从内核传递给应用态应用。
procfs一般用于向用户出口少量的数据信息,或用户通过它设置内核变量从而控制内核行为。
seq_file实际上依赖于 procfs,因此为了使用seq_file,必须使内核支持procfs。
debugfs用于内核开发者调试使用,它比其他集中方式都方便,但是仅用于简单类型的变量处理。
relayfs是一种非常复杂的数据交换方式,要想准确使用并不容易,但是如果使用得当,它远比procfs和seq_file功能强大。
**
__________________________________________________________________________________________
解决IP地址到__be32转换问题


__ue32 inet_addr(const char *);//不过是在用户态用的,内核态还没找到
如:
message.packet_ip_info.ip_addr=inet_addr("192.168.0.1");

____________________________________________________________

解决NL读取用户空间传来的自定义数据结构问题


memcpy(void*,void*,size_t);
如:这里用到了NLMSG常用的宏来提取数据大小
memcpy(&packet_from_user,NLMSG_DATA(nlh), sizeof(struct packet_info));

_________________________________________________________________
解决JNI与C参数传递
java 对象与 c结构体进行交互:

jobjectArray args=0;
//array size
jsize len=20;
int len_c=20;
jclass objClass = (*env)->FindClass(env,"java/lang/Object");
//create object arrary
args = (*env)->NewObjectArray(env,len, objClass, 0);
jclass objectClass = (*env)->FindClass(env,"packet/DiskInfo");
jfieldID ipsrc = (*env)->GetFieldID(env,objectClass,"ipsrc","Ljava/lang/String;");
jfieldID ipdest = (*env)->GetFieldID(env,objectClass,"ipdest","Ljava/lang/String;");
jfieldID protocol = (*env)->GetFieldID(env,objectClass,"protocol","Ljava/lang/String;");

//清理前个对象工作
jobject objTemp = (*env)->AllocObject(env,objectClass);
(*env)->SetObjectField(env,_obj,ipdest,(*env)->NewStringUTF(env,inet_ntoa(addr)));

(*env)->SetObjectArrayElement(env,args, i, objTemp);

return args;

实现ip数据包信息返回给上层ui

__________________________________________________________________
解决过滤ip存储问题
用的链表,比较粗糙,并想结合端口过滤一起做进去,通过标示符来判别是什么操作




1 楼 idealab 2009-04-20 同寝室也有人做你这个毕设题目,他用QT开发界面,底层调用一些开源实现。 2 楼 C_J 2009-04-20 idealab 写道
同寝室也有人做你这个毕设题目,他用QT开发界面,底层调用一些开源实现。

我没用第三方库,那样可能会省事很多,毕竟不要考虑与内核进行交互了。
选java也是为了学习点jni接口,以后可能有兴趣参与另一个开源项目。因为只提供了一个so库。 3 楼 fishrain_ 2009-05-09 我的题目也是这个,第一次在LINUX下开发项目。由于前一个多月在忙于其它事,所以没有太研究,很是被动。最终我还是直接调用调用第三方库了。

我用GTKMM做界面,下面直接转化为iptables,搞了半个月了,还只是把界面给弄出来。



可否给我点建议,或者推荐我看一些项目,文档之类的,谢谢了!!

PS:我想在防火墙里加入网络监视这个功能(对局域网中各计算机进包/出包进行监视),这个要怎么弄呢? 4 楼 C_J 2009-05-12 fishrain_ 写道
我的题目也是这个,第一次在LINUX下开发项目。由于前一个多月在忙于其它事,所以没有太研究,很是被动。最终我还是直接调用调用第三方库了。我用GTKMM做界面,下面直接转化为iptables,搞了半个月了,还只是把界面给弄出来。可否给我点建议,或者推荐我看一些项目,文档之类的,谢谢了!!PS:我想在防火墙里加入网络监视这个功能(对局域网中各计算机进包/出包进行监视),这个要怎么弄呢?


其实netfliter本身提供了一套扩展性很强的东西
只需要不断挂hook函数就可以了

关于监视,你是不是说的连接跟踪? 博客和"综合技术"版面有个我的帖子(在cu上也发了一个帖子http://linux.chinaunix.net/bbs/thread-1098391-1-5.html)说明了我的实现方法,其实也是直接用的 /proc/net/nf_conntrack 获取到的内核网络包信息,并没自己重新做内核模块。

我没有用到iptables,其实iptables也是个用户级的模块。

这几天也是有点忙,一直没时间上博客了。呵呵

5 楼 fishrain_ 2009-05-14 做好了呀!!我的还在途中呢,期待你的项目,想借鉴一下!

我的GTKMM+iptables+netfilter,真让人头疼

读书人网 >UNIXLINUX

热点推荐