学会了内核和用户空间的交互,之后我们便可以正式的完善我们的用户空间程序和内核,让它变得更加完整,切库其实本质上是切换ip/port二元组。所以我们需要一个数据结构来存放,这个数据结构是内核和用户空间都需要用到的,所以我们单独抽出来放到一个头文件中nc_def.h
/*
* nc_def.h
*/
#ifndef __NC_DEF_H
#define __NC_DEF_H
struct nc_distort {
unsigned int old_addr;
unsigned int new_addr;
unsigned short old_port;
unsigned short new_port;
};
#define procfs_name "netcco"
#endif
有些人可能会问了,哎呀为什么addr(也就是ip)是一个int呀?我们所熟知的ip不是192.168.x.x这种形式吗?仔细看的话,其实每一个位都是0~255也就是2^8,一个字节。四位就是四个字节,所以我们只需要找一个大于四个字节的类型就可以了,int不是很好吗。当然你把它精确定义成int32类型是很好的选择。
然后我们写一个用户程序,首先接受用户的参数,写入/proc/netcco。
/*
* netccoctl.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include "nc_def.h"
static char proc_file[256];
static struct nc_distort d;
int main(int argc, char** argv){
sprintf(proc_file, "/proc/%s", procfs_name);
int fd = open(proc_file, O_WRONLY);
if (fd<0) {
perror("open");
return EXIT_FAILURE;
}
if (argc<5) {
printf("%s [old_ip] [old_port] [new_ip] [new_port]\n", argv[0]);
return EXIT_FAILURE;
}
//inet_addr把xxx.xxx.xxx.xx的字符串直接变成整形(网络字节序)
d.old_addr = inet_addr(argv[1]);
d.old_port = htons(atoi(argv[2]));
d.new_addr = inet_addr(argv[3]);
d.new_port = htons(atoi(argv[4]));
printf("old ip:%d\n", d.old_addr);
printf("old port:%d\n", d.old_port);
printf("new ip:%d\n", d.new_addr);
printf("new port:%d\n", d.new_port);
write(fd, &d, sizeof(struct nc_distort));
close(fd);
return EXIT_SUCCESS;
}
接下来,我们修改一下内核模块代码,把之前那个 char buf[80]改成这个结构的类型变量,同时我们在write函数中输出。
#define MAX_DISTORTS (100)
static struct nc_distort distort_buf; //这里的char buf[80]就被替换了
static int distorts_len;
static struct nc_distort distorts[MAX_DISTORTS];
static ssize_t proc_op_write(struct file *filp,
const char __user *buffer,
size_t len,
loff_t* offset)
{
if(copy_from_user(&distort_buf, buffer, len)){
return -ENOMEM;
}
printk(KERN_INFO "[netcco]old ip:%d\n", distort_buf.old_addr);
printk(KERN_INFO "[netcco]old port:%d\n", distort_buf.old_port);
printk(KERN_INFO "[netcco]new ip:%d\n", distort_buf.new_addr);
printk(KERN_INFO "[netcco]new ip:%d\n", distort_buf.new_port);
//写入到distorts
distorts[distorts_len].old_addr = distort_buf.old_addr;
distorts[distorts_len].old_port = distort_buf.old_port;
distorts[distorts_len].new_addr = distort_buf.new_addr;
distorts[distorts_len].new_port = distort_buf.new_port;
distorts_len++;
return len;
}
然后修改我们的Makefile,让它在编译内核模块的同时也编译我们的netccoctl。
obj-m += netcco.o
all: netccoctl
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
rm netccoctl
netccoctl: netccoctl.c nc_def.h
最后我们make一下,应该能看到除了ko文件还有一个netccoctl可执行文件。
$> make
$> sudo insmod netcco.ko #先安装模块,才会出现proc下面的netcco虚拟文件
$> ./netccoctl 118.24.223.24 8989 121.22.22.23 8888
old ip:417273974
old port:7459
new ip:387323513
new port:47138
$> tail /var/log/kern.log
Oct 14 12:11:40 Sjet kernel: [ 2800.381019] [netcco]old ip:417273974
Oct 14 12:11:40 Sjet kernel: [ 2800.381020] [netcco]old port:7459
Oct 14 12:11:40 Sjet kernel: [ 2800.381021] [netcco]new ip:387323513
Oct 14 12:11:40 Sjet kernel: [ 2800.381021] [netcco]new ip:47138
$> sudo rmmod netcco #卸载模块
然后进入重头戏——netfilter了,netfilter是linux内核高效的网络协议栈的hack点,很多路由器和网络中继设备以及防火墙和代理产品都是构建于netfilter。这里就不展开讲了,对netfilter有疑问的可以搜索一下相关资料,总之netfilter对内核消费一个网络包的整个生命周期这个过程暴露了5个注入点钩子,每个网络数据包来内核都会在不同时期上面调用钩子里面的函数以此来决定是否继续让数据包往下走。我们只关心刚进来和最终出去的两个钩子就够了,俗称掐头去尾。中间的钩子,大家自行学习。同proc一样,也是一顿注册回调到某个ops里面,在把这个ops绑定(注册)到netfilter。
int nf_register_net_hook(struct net *net, const struct nf_hook_ops *ops);
第一个参数其实不需要了解,是namespace。有一个默认的函数可以使用不需要我们自己去写。第二个就是对应的ops了,还记得上一篇博客用来操作proc文件的file_ops吗?差不多一回事。我们来看一下它的长相
struct nf_hook_ops {
/* User fills in from here down. */
nf_hookfn *hook;
struct net_device *dev;
void *priv;
u_int8_t pf;
unsigned int hooknum;
/* Hooks are ordered in ascending priority. */
int priority;
};
相比file_ops它就显得友好的多,其中hook就是我们的处理函数,而hooknum就是5个注入点的具体哪个。dev是网卡设备比如“lo/eth0/ws4p0”,我们不用管。priority是优先级,也就是同样的注入点有多个ops,那么如何排序。
按照我们第0章写的,我们不妨先把三个规则函数写出来
#define NC_UNMATCHED (0)
#define NC_MATCHED (1)
static struct iphdr *ip_header; //ip帧header,ip信息位于这里
static struct tcphdr *tcp_header;//tcp帧header,端口信息位于这里
// 规则一
// 阻挡老ip/port过来的数据包
static int block_old_source(const struct nc_distort distort)
{
if (ip_header->saddr==distort.old_addr&&
tcp_header->source==distort.old_port) {
return NC_MATCHED;
}
return NC_UNMATCHED;
}
// 规则二
// 进来的包如果是新ip/port,需要改回老ip/port
static int incoming_distort_source(const struct nc_distort distort)
{
if (ip_header->saddr==distort.new_addr&&
tcp_header->source==distort.new_port) {
ip_header->saddr = distort.old_addr;
tcp_header->source = distort.old_port;
return NC_MATCHED;
}
return NC_UNMATCHED;
}
// 规则三
// 出去的包,如果是老ip/port,需要改成新ip/port
static int outgoing_distort_dest(const struct nc_distort distort)
{
if (ip_header->daddr==distort.old_addr&&
tcp_header->dest==distort.old_port) {
ip_header->daddr = distort.new_addr;
tcp_header->dest = distort.new_port;
return NC_MATCHED;
}
return NC_UNMATCHED;
}
接着就是两个netfilter钩子函数,一进一出,但由于我们可能修改了数据库,修改数据包之后一个很重要的动作就是重新签名封包。还需要一个额外的函数用来干这个活。
static void update_chksum(struct sk_buff *skb, struct iphdr *iph, struct tcphdr *tcp)
{
//ip
iph->check = 0;
iph->check = ip_fast_csum(iph,iph->ihl);
//tcp
tcp->check = 0;
tcp->check = csum_tcpudp_magic(iph->saddr, iph->daddr,
ntohs(iph->tot_len)-iph->ihl*4, IPPROTO_TCP,
csum_partial(tcp, ntohs(iph->tot_len)-iph->ihl*4, 0));
//checksum
skb->ip_summed = CHECKSUM_NONE;
}
static unsigned int _hook_incoming(void* priv,
struct sk_buff* skb,
const struct nf_hook_state* st)
{
skb_linearize(skb);
ip_header = ip_hdr(skb);
tcp_header = tcp_hdr(skb);
if (ip_header->protocol==IPPROTO_TCP) {
for (i = 0;i<distorts_len;i++) {
if (block_old_source(distorts[i])) {
return NF_DROP;
}
if (incoming_distort_source(distorts[i])) {
update_chksum(skb, ip_header, tcp_header);
return NF_ACCEPT;
}
}
}
return NF_ACCEPT;
}
static unsigned int _hook_outgoing(void* priv,
struct sk_buff* skb,
const struct nf_hook_state* st)
{
skb_linearize(skb);
ip_header = ip_hdr(skb);
tcp_header = tcp_hdr(skb);
if (ip_header->protocol==IPPROTO_TCP) {
for (i=0;i<distorts_len;i++) {
if (outgoing_distort_dest(distorts[i])) {
update_chksum(skb, ip_header, tcp_header);
return NF_ACCEPT;
}
}
}
return NF_ACCEPT;
}
最终,把钩子注册到netfilter内核中去。
static struct nf_hook_ops i_hook;
static struct nf_hook_ops o_hook;
static void install_hooks(void){
i_hook.hook = _hook_incoming;
i_hook.hooknum = NF_INET_PRE_ROUTING;
i_hook.pf = PF_INET;
i_hook.priority = NF_IP_PRI_FIRST;
nf_register_net_hook(&init_net, &i_hook);
o_hook.hook = _hook_outgoing;
o_hook.hooknum = NF_INET_POST_ROUTING;
o_hook.pf = PF_INET;
o_hook.priority = NF_IP_PRI_FIRST;
nf_register_net_hook(&init_net, &o_hook);
}
static void uninstall_hooks(void){
nf_unregister_net_hook(&init_net, &i_hook);
nf_unregister_net_hook(&init_net, &o_hook);
}
然后把这两个函数分别放到模块的初始化/卸载函数中去,就大功告成了。我们来看一下是否生效了呢。首先我在我自己的云vps上面部署了一个网页。然后我们使用netcco修改一个不存在的ip/port到我自己的vps上面。
$> sudo insmod netcco.ko
$> ./netccoctl 118.24.223.22 3999 118.24.xx.xxx 8000
接着我们打开浏览器访问一下这个并不存在的ip地址端口3999

6219

被折叠的 条评论
为什么被折叠?



