前言
我们经常有这么一个需求,我们可以通过ip地址来登录我们的设备。但是如果ip地址变化,我们又不知道设备的ip地址的时候,就不能够正常登录到设备,或者很难登录到设备(需要查看设备当前的ip地址才可以)。那么能不能实现我可以通过一个域名来登录设备(即:给设备绑定一个随意的域名,通过域名来访问设备)
linux内核数据包桥流程分析
这里引用网上比较好的博客,让大家对于linux桥接有一个直观的认识
什么是桥接?
简单来说,桥接就是把一台机器上的若干个网络接口“连接”起来。其结果是,其中一个网口收到的报文会被复制给其他网口并发送出去。以使得网口之间的报文能够互相转发。 交换机就是这样一个设备,它有若干个网口,并且这些网口是桥接起来的。于是,与交换机相连的若干主机就能够通过交换机的报文转发而互相通信。
交换机在报文转发的过程中并不会篡改报文数据,只是做原样复制。然而桥接却并不是在物理层实现的,而是在数据链路层。交换机能够理解数据链路层的报文,所以实际上桥接却又不是单纯的报文转发。 交换机会关心填写在报文的数据链路层头部中的Mac地址信息(包括源地址和目的地址),以便了解每个Mac地址所代表的主机都在什么位置(与本交换机的哪个网口相连)。在报文转发时,交换机就只需要向特定的网口转发即可,从而避免不必要的网络交互。这个就是交换机的“地址学习”。但是如果交换机遇到一个自己未学习到的地址,就不会知道这个报文应该从哪个网口转发,则只好将报文转发给所有网口(接收报文的那个网口除外)。 比如主机C向主机A发送一个报文,报文来到了交换机S1的eth2网口上。假设S1刚刚启动,还没有学习到任何地址,则它会将报文转发给eth0和eth1。同时,S1会根据报文的源Mac地址,记录下“主机C是通过eth2网口接入的”。于是当主机A向C发送报文时,S1只需要将报文转发到eth2网口即可。而当主机D向C发送报文时,假设交换机S2将报文转发到了S1的eth2网口(实际上S2也多半会因为地址学习而不这么做),则S1会直接将报文丢弃而不做转发(因为主机C就是从eth2接入的)。
然而,网络拓扑不可能是永不改变的。假设我们将主机B和主机C换个位置,当主机C发出报文时(不管发给谁),交换机S1的eth1口收到报文,于是交换机S1会更新其学习到的地址,将原来的“主机C是通过eth2网口接入的”改为“主机C是通过eth1网口接入的”。 但是如果主机C一直不发送报文呢?S1将一直认为“主机C是通过eth2网口接入的”,于是将其他主机发送给C的报文都从eth2转发出去,结果报文就发丢了。所以交换机的地址学习需要有超时策略。对于交换机S1来说,如果距离最后一次收到主机C的报文已经过去一定时间了(默认为5分钟),则S1需要忘记“主机C是通过eth2网口接入的”这件事情。这样一来,发往主机C的报文又会被转发到所有网口上去,而其中从eth1转发出去的报文将被主机C收到。
linux桥接
linux内核支持网口的桥接(目前只支持以太网接口)。但是与单纯的交换机不同,交换机只是一个二层设备,对于接收到的报文,要么转发、要么丢弃。小型的交换机里面只需要一块交换芯片即可,并不需要CPU。而运行着linux内核的机器本身就是一台主机,有可能就是网络报文的目的地。其收到的报文除了转发和丢弃,还可能被送到网络协议栈的上层(网络层),从而被自己消化。 linux内核是通过一个虚拟的网桥设备来实现桥接的。这个虚拟设备可以绑定若干个以太网接口设备,从而将它们桥接起来。
数据包接收过程(如下图所示) 从图中我们可以看到桥虚拟接口br0下面挂载了两个接口eth0和wlan0。当数据包从eth0接口收到后数据包会被送到虚拟网桥接口br0。桥处的代码会判断是否采用二层转发还是3层转发。如果是二层转发,那么数据包就不会送到网络层,直接会在br0下挂的设备寻找匹配的接口,然后从该匹配的接口转发。如果数据包不满足二层转发条件,数据包就会被送到3层。3层代码又会判断是发送到本机还是需要本机转发,如果是发送到本机,则设备本身接收,如果需要路由转发,则查找路由表,然后转发。
数据包发送流程 当本地发送的数据包或者网络转发的数据包会被送到br0。桥相关的代码会判断向挂载到br0下面的哪一个接口转发,将数据包从合适的接口转发出去。
linux桥相关代码
数据包流向
接收数据包 netif_rx–>netif_rx_schedule–>net_rx_action–>process_backlog–>netif_receive_skb–>上层协议栈处理发送数据包fun_xmit–>dev_queue_xmit–>qdisc_run–>qdsic_restart–>net_tx_action–>dev->hard_start_xmit–>发送完成
桥入口代码行数为int netif_receive_skb(struct sk_buff skb)* //网卡收到包会调用该函数将数据包向协议栈上扔 int netif_receive_skb(struct sk_buff skb) //网卡收到包会调用该函数将数据包向协议栈上扔,然后会调用br_handle_frame函数,让桥接的代码来处理这个报文。该函数就是桥的入口函数*
static int __netif_receive_skb(struct sk_buff
*skb
)
static int __netif_receive_skb_core(struct sk_buff
*skb
, bool pfmemalloc
)
rx_handler
= rcu_dereference(skb
->dev
->rx_handler
);
if (rx_handler
) {
if (pt_prev
) {
ret
= deliver_skb(skb
, pt_prev
, orig_dev
);
pt_prev
= NULL;
}
switch (rx_handler(&skb
))
那么这里产生了一个问题,br_handle_frame是什么时候注册到内核的呢。带着该问题我们来看一下代码。
int br_add_if(struct net_bridge
*br
, struct net_device
*dev
)
err
= netdev_rx_handler_register(dev
, br_handle_frame
, p
);
int netdev_rx_handler_register(struct net_device
*dev
,
rx_handler_func_t
*rx_handler
,
void *rx_handler_data
)
{
ASSERT_RTNL();
if (dev
->rx_handler
)
return -EBUSY
;
rcu_assign_pointer(dev
->rx_handler_data
, rx_handler_data
);
rcu_assign_pointer(dev
->rx_handler
, rx_handler
);
return 0;
}
我们可以看到,当调用底层程序br_add_if的时候,会调用netdev_rx_handler_register。netdev_rx_handler_register中初始化了dev->rx_handler处理程序。
那么又产生了一个问题,什么时候会调用br_add_if函数呢? 在这里我们不妨来看看应用程序初始化桥和给桥添加接口的应用代码。 brtcl addbr br0 (建立一个网桥br0, 同时在Linux内核里面创建虚拟网卡br0) brtcl addif br0 eth0 brtcl addif br0 wlan0 通过这里我们可以看到给br0添加接口的时候就会调用操作系统底层的br_add_if。 我们自己的DNS拦截和修改代码就添加在br_handle_frame代码中
dns拦截程序及分析
正如上所说,我们的拦截代码是放在br_handle_frame中的,那么下面我们来看一下br_handle_frame的代码实现
int (*g_fn_dnstrap
)(struct sk_buff
*skb
) = NULL;
EXPORT_SYMBOL(g_fn_dnstrap
);
rx_handler_result_t
br_handle_frame(struct sk_buff
**pskb
)
if (g_fn_dnstrap
!= NULL)
{
g_fn_dnstrap(skb
);
}
在这里我们可以看到如果g_fn_dnstrap函数指针不为空,我们则调用该函数指针指向的函数。那么该函数指针什么时候被赋值呢?下面来看看我们的内核模块的代码。
dns内核代码
TODO:该内核模块非常重要,请仔细阅读
DNS报文结构定义和分析
DNS占用53号端口,同时使用TCP和UDP协议。那么DNS在什么情况下使用这两种协议? DNS在区域传输的时候使用TCP协议,其他时候使用UDP协议。 DNS区域传输的时候使用TCP协议:
1.辅域名服务器会定时(一般3小时)向主域名服务器进行查询以便了解数据是否有变动。如有变动,会执行一次区域传送,进行数据同步。区域传送使用TCP而不是UDP,因为数据同步传送的数据量比一个请求应答的数据量要多得多。
2.TCP是一种可靠连接,保证了数据的准确性。 域名解析时使用UDP协议:
客户端向DNS服务器查询域名,一般返回的内容都不超过512字节,用UDP传输即可。不用经过三次握手,这样DNS服务器负载更低,响应更快。理论上说,客户端也可以指定向DNS服务器查询时用TCP,但事实上,很多DNS服务器进行配置的时候,仅支持UDP查询包。
TODO: 该头部是DNS的头部信息,发送和接收该头部的长度是固定的 详情查看https://blog.csdn.net/liao152/article/details/45252387
我们来看看dns头部信息我们是怎么定义的
typedef struct _header
{
unsigned short int id
;
unsigned short u
;
short int qdcount
;
short int ancount
;
short int nscount
;
short int arcount
;
} dnsheader_t;
对应着下图中的各字段。
内核模块加载函数分析:
#include "dns_packet.h"
#include "dns_proc.h"
typedef int (*fn_dnstrap
)(struct sk_buff
*skb
);
extern fn_dnstrap g_fn_dnstrap
;
static int __init
dnstrap_init(void) {
create_dnstrap_proc();
g_fn_dnstrap
= br_dns_trap_enter
;
return 0;
}
static void __exit
dnstrap_eixt(void) {
destroy_dnstrap_proc();
g_fn_dnstrap
= NULL;
}
MODULE_LICENSE("GPL");
module_init(dnstrap_init
);
module_exit(dnstrap_eixt
);
proc文件创建代码分析
#include "dns_common.h"
#define PROC_ROOT "dns_filter"
#define PROC_DOMAIN_NAME "domain_name"
#define PROC_ENABLE "enable"
void create_dnstrap_proc();
void destroy_dnstrap_proc();
#include "dns_proc.h"
bool g_dns_filter_enable
= false
;
unsigned char g_domain_name
[80] = {0};
static struct proc_dir_entry
*dnstrap_proc_root
= NULL;
static int dnstrap_domain_read(struct seq_file
*s
, void *v
) {
seq_printf(s
, "%s\n", g_domain_name
);
return 0;
}
static int dnstrap_domain_proc_open(struct inode
*inode
, struct file
*file
) {
return (single_open(file
, dnstrap_domain_read
, NULL));
}
static int dnstrap_domain_write(struct file
*file
, const char *buffer
,
unsigned long count
, void *data
) {
if (count
< 2) return -EFAULT
;
if (buffer
&& !copy_from_user(g_domain_name
, buffer
, 80)) {
g_domain_name
[count
- 1] = 0;
str_to_lower(g_domain_name
);
return count
;
}
return -EFAULT
;
}
int dnstrap_domain_proc_write(struct file
*file
, const char __user
*userbuf
,
size_t count
, loff_t
*off
) {
return dnstrap_domain_write(file
, userbuf
, count
, off
);
}
struct file_operations dnstrap_domain_proc_fops
= {
.open
= dnstrap_domain_proc_open
,
.write
= dnstrap_domain_proc_write
,
.read
= seq_read
,
.llseek
= seq_lseek
,
.release
= single_release
,
};
static int dnstrap_en_write(struct file
*file
, const char *buffer
,
unsigned long count
, void *data
) {
char tmpbuf
[80];
if (count
< 2) return -EFAULT
;
if (buffer
&& !copy_from_user(tmpbuf
, buffer
, count
)) {
tmpbuf
[count
] = '\0';
if (tmpbuf
[0] == '0')
g_dns_filter_enable
= true
;
else if (tmpbuf
[0] == '1')
g_dns_filter_enable
= false
;
return count
;
}
return -EFAULT
;
}
static int dnstrap_en_read(struct seq_file
*s
, void *v
) {
seq_printf(s
, "%d\n", g_dns_filter_enable
);
return 0;
}
static int dnstrap_en_proc_open(struct inode
*inode
, struct file
*file
) {
return (single_open(file
, dnstrap_en_read
, NULL));
}
static int dnstrap_en_proc_write(struct file
*file
, const char __user
*userbuf
,
size_t count
, loff_t
*off
) {
return dnstrap_en_write(file
, userbuf
, count
, off
);
}
struct file_operations dnstrap_en_proc_fops
= {
.open
= dnstrap_en_proc_open
,
.write
= dnstrap_en_proc_write
,
.read
= seq_read
,
.llseek
= seq_lseek
,
.release
= single_release
,
};
void create_dnstrap_proc() {
dnstrap_proc_root
= proc_mkdir(PROC_ROOT
, NULL);
if (dnstrap_proc_root
) {
proc_create_data(PROC_DOMAIN_NAME
, 0, dnstrap_proc_root
,
&dnstrap_domain_proc_fops
, NULL);
proc_create_data(PROC_ENABLE
, 0, dnstrap_proc_root
, &dnstrap_en_proc_fops
,
NULL);
}
}
void destroy_dnstrap_proc() {
if (dnstrap_proc_root
) {
remove_proc_entry(PROC_DOMAIN_NAME
, &dnstrap_proc_root
);
remove_proc_entry(PROC_ENABLE
, &dnstrap_proc_root
);
remove_proc_entry(PROC_ROOT
, NULL);
}
}
DNS TRAP代码(非常关键的代码)
DNS抓包图
dns请求报文 dns应答报文截图
#include "dns_packet.h"
extern unsigned char g_domain_name
[80];
static unsigned char dns_answer
[] = {0xC0, 0x0C, 0x00, 0x01, 0x00, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x04};
static int get_domain_name(unsigned char *dns_body
, char *domain_name
,
int body_len
) {
int offset
= 0, token_len
= 0;
char token
[64] = {0};
char domain
[128] = {0};
short type
;
if (!dns_body
|| !domain_name
|| body_len
<= 0) {
return -1;
}
while (body_len
> 0) {
memset(token
, 0, sizeof(token
));
token_len
= dns_body
[offset
];
if ((token_len
> 0) && (token_len
<= body_len
)) {
strncpy(token
, dns_body
+ offset
+ 1, token_len
);
if (!domain
[0]) {
strncpy(domain
, token
, (sizeof(token
) - 1));
} else {
strncat(domain
, ".", (sizeof(domain
) - strlen(domain
) - 1));
strncat(domain
, token
, (sizeof(domain
) - strlen(domain
) - 1));
}
} else {
if (token_len
> body_len
)
printk("%s[%d], token_len is %d, body_len is %d\n", __FUNCTION__
,
__LINE__, token_len
, body_len
);
break;
}
token_len
+= 1;
body_len
-= token_len
;
offset
+= token_len
;
}
strncpy(domain_name
, domain
, (sizeof(domain
) - 1));
return 0;
}
static bool
is_valid_dns_query_header(dnsheader_t
*dns_header
) {
if (dns_header
== NULL) {
return false
;
}
if (dns_header
->qdcount
< 1) {
return false
;
}
if (((dns_header
->u
& 0x8000) >> 15) !=
0)
{
return false
;
}
if (((dns_header
->u
& 0x7100) >> 11) !=
0)
{
return false
;
}
if (((dns_header
->u
& 0x70) >> 4) != 0)
{
return false
;
}
return true
;
}
static bool
is_domain_name_equal(char *domain_name1
, char *domain_name2
) {
char temp1
[128];
char temp2
[128];
if (!domain_name1
|| !domain_name2
) {
return false
;
}
str_to_lower(domain_name1
);
str_to_lower(domain_name2
);
if (!strncmp(domain_name1
, "www.", 4)) {
strcpy(temp1
, domain_name1
+ 4);
} else {
strcpy(temp1
, domain_name1
);
}
if (!strncmp(domain_name2
, "www.", 4)) {
strcpy(temp2
, domain_name2
+ 4);
} else {
strcpy(temp2
, domain_name2
);
}
if (strcmp(temp1
, temp2
))
return false
;
else
return true
;
}
int br_dns_packet_recap(struct sk_buff
*skb
) {
struct iphdr
*iph
;
struct udphdr
*udph
;
struct net_device
*br0_dev
;
struct in_device
*br0_in_dev
;
dnsheader_t
*dns_pkt
;
unsigned char mac
[ETH_ALEN
];
unsigned int ip
;
unsigned short port
;
unsigned char *ptr
= NULL;
int extend_len
;
extend_len
= EXTEND_LEN_V4
;
br0_dev
= dev_get_by_name(&init_net
, "br0");
br0_in_dev
= in_dev_get(br0_dev
);
if (!br0_dev
|| !br0_in_dev
) {
if (br0_in_dev
) in_dev_put(br0_in_dev
);
if (br0_dev
) dev_put(br0_dev
);
return -1;
}
if (!br0_in_dev
->ifa_list
) {
in_dev_put(br0_in_dev
);
dev_put(br0_dev
);
return -1;
}
iph
= ip_hdr(skb
);
udph
= (void *)iph
+ iph
->ihl
* 4;
dns_pkt
= (void *)udph
+ sizeof(struct udphdr
);
ptr
= (void *)udph
+ ntohs(udph
->len
);
skb_put(skb
, extend_len
);
memcpy(mac
, eth_hdr(skb
)->h_dest
, ETH_ALEN
);
memcpy(eth_hdr(skb
)->h_dest
, eth_hdr(skb
)->h_source
, ETH_ALEN
);
memcpy(eth_hdr(skb
)->h_source
, mac
, ETH_ALEN
);
ip
= iph
->saddr
;
iph
->saddr
= iph
->daddr
;
iph
->daddr
= ip
;
iph
->tot_len
= htons(ntohs(iph
->tot_len
) + extend_len
);
port
= udph
->source
;
udph
->source
= udph
->dest
;
udph
->dest
= port
;
udph
->len
= htons(ntohs(udph
->len
) + extend_len
);
dns_pkt
->u
= htons(0x8180);
dns_pkt
->qdcount
= htons(1);
dns_pkt
->ancount
= htons(1);
dns_pkt
->nscount
= htons(0);
dns_pkt
->arcount
= htons(0);
memcpy(ptr
, dns_answer
, 12);
memcpy(ptr
+ 12, (unsigned char *)&br0_in_dev
->ifa_list
->ifa_address
, 4);
skb
->ip_summed
= CHECKSUM_NONE
;
iph
->check
= 0;
iph
->check
= ip_fast_csum((unsigned char *)iph
, iph
->ihl
);
udph
->check
= 0;
udph
->check
=
csum_tcpudp_magic(iph
->saddr
, iph
->daddr
, ntohs(udph
->len
), IPPROTO_UDP
,
csum_partial((char *)udph
, ntohs(udph
->len
), 0));
if (br0_in_dev
) {
in_dev_put(br0_in_dev
);
}
if (br0_dev
) {
dev_put(br0_dev
);
}
return 1;
}
int br_dns_trap_enter(struct sk_buff
*skb
) {
struct iphdr
*iph
;
struct udphdr
*udph
;
iph
= (struct iphdr
*)skb_network_header(skb
);
if (iph
->version
!= 4) {
return -1;
}
udph
= (void *)iph
+ iph
->ihl
* 4;
if (iph
->protocol
!= IPPROTO_UDP
|| ntohs(udph
->dest
) != 53) {
return -1;
}
int domain_name_len
=
ntohs(udph
->len
) - sizeof(struct udphdr
) - sizeof(dnsheader_t
) - 4;
if (domain_name_len
<= 1) {
return -1;
}
dnsheader_t
*dns_hdr
= (dnsheader_t
*)((void *)udph
+ sizeof(struct udphdr
));
if (is_valid_dns_query_header(dns_hdr
) != true
) {
return -1;
}
unsigned char *body
=
(void *)udph
+ sizeof(struct udphdr
) + sizeof(dnsheader_t
);
unsigned char domain_name
[128] = {0};
if (get_domain_name(body
, domain_name
, domain_name_len
) != 0) {
return -1;
}
if (is_domain_name_equal(domain_name
, g_domain_name
) == false
) {
return -1;
}
br_dns_packet_recap(skb
);
}
代码地址:代码仓库地址
未完待续,该代码我会在后续加入ipv6的截图和分析以及劫持。上面的代码已经测试过,能够跳转。这次的代码只是DNS Trap中redirect的一种写法,我会在后面的博客中提供多种redirect写法以及filter写法。欢迎关注博客,欢迎加入QQ群610849576交流