精华内容
下载资源
问答
  • 我们自己的DNS拦截和修改代码就添加在br_handle_frame代码中 dns拦截程序及分析 正如上所说,我们的拦截代码是放在br_handle_frame中的,那么下面我们来看一下br_handle_frame的代码实现 // 下面的两句代码是我加的...

    前言

    我们经常有这么一个需求,我们可以通过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)
    	//这里就会从rcu_dereference获取到rx_handler指向的函数指针,那么该函数又是在什么地方初始化的呢
    	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
    

    那么这里产生了一个问题,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;
    
    	/* Note: rx_handler_data must be set before rx_handler */
    	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的代码实现
    // 下面的两句代码是我加的,因为这里没有hook点,所以自己
    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; //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) {
      //创建proc文件
      create_dnstrap_proc();
      //我们可以看到这里将g_fn_dnstrap进行了赋值,所以重点函数为br_dns_trap_enter,我们将在后面对于该函数进行分析
      g_fn_dnstrap = br_dns_trap_enter;
      return 0;
    }
    
    static void __exit dnstrap_eixt(void) {
      //销毁proc文件
      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;
    // 该全局变量从pro文件中获取用户输入的url域名
    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;
    //从用户空间拷贝domain_name
      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,
    };
    
    //该函数是设置是否开启dns trap的功能
    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];
    //我们可以从dns应答报文截图中可以看到我们访问的www.scdn.com返回了csdn的ip地址。该ip地址和域名在Answers字段中。同时可以抓取其他的数据包可以看到前12个字节是固定的,后4个字节对应的是域名所对应的ip地址。
    static unsigned char dns_answer[] = {0xC0, 0x0C, 0x00, 0x01, 0x00, 0x01,
                                         0x00, 0x00, 0x00, 0x00, 0x00, 0x04};
    
    //该函数没有什么分析的,就是从dns请求报文中截取出访问的url地址。
    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;
    }
    
    //判断是否是合法的dns请求头,关于dns的请求头部的每一个字段,可以查看上面贴出的dns报文信息的链接地址。这里其实可以少些判断,当然判断的越多越准确,主要是按照协议来的。
    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) /*QR: query should be 0,answer be 1*/
      {
        return false;
      }
      if (((dns_header->u & 0x7100) >> 11) !=
          0) /*opcode: 0:standard,1:reverse,2:server status*/
      {
        return false;
      }
      if (((dns_header->u & 0x70) >> 4) != 0) /*Z: reserved, should be 0*/
      {
        return false;
      }
      return true;
    }
    
    
    //该函数是判断我们通过应用层下发的proc中的domain_name和我们从dns报文中截取的是否相等
    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;
    }
    
    // TODO: 该函数是核心函数,设计到数据包的修改,该函数是dns报文的修改和重定向函数
    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;
      /*
      关于net_device和in_device详见https://blog.csdn.net/sinat_20184565/article/details/79898433
      我们这里的需要返回br0当前的ip地址,所以需要获取用户给br0配置的ip地址,所以需要该函数
      TODO: 用完该结构我们必须要调用put来进行释放
      */
      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;
      }
      /*
        in_ifaddr表示地址结构,其成员包含了地址,掩码,范围等信息,多个地址连接成链表,主地址在前,从地址在后;
        struct in_ifaddr    *ifa_list;    /* IP ifaddr chain
        如果br0接口配置的有ip地址,子网掩码等,所有的信息都存放在这里(我们可以从该结构中拿到当前的br0的ip地址)
    */
    
      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为尾部指针,指向了dns请求报文的尾部
      ptr = (void *)udph + ntohs(udph->len);
      /*
      将数据段向后扩大extend_len的长度(这里是16个字节,ip地址为4个字节,固定长度12个字节)
      该条语句的操作可以参考wireshark抓包。
      TODO:下面这句话是该整个修改数据包函数的核心思想
      TODO:dns的回复报文需要在请求报文(queries字段)的后面添加应答字段(answers),具体可以查看
            上面的博客连接和通过抓包分析
            answers字段中的前12个字段是固定的,12个字段后面接的是ip地址
            所以是16个字节
      */
      skb_put(skb, extend_len);
    
      // TODO:下面的操作其实都是在操作五元组,内核中数据包的流向都是靠五元组(源)
      /*
              源IP地址、目的IP地址、协议号、源端口、目的端口(协议号已经是有的了)
      */
    
      //交换mac地址
      /*
        在网络通信的是否,数据包中的地址变化过程如下:(不在同一局域网)
        源mac地址为当前设备的mac地址,目的mac是下一条设备的mac地址
        目的ip不变,源ip地址是为当前设备的ip地址。
    
        在这里的理解为:
        当pc访问www.baidu.com的时候,首先会向网关发送dns请求。(此时的源mac地址为pc的mac,目的mac地址为网关。源ip地址为pc的ip地址,目的ip地址为网关的ip地址)
        当我们在网关拦截到数据包的时候,需要交换数据报文中的mac地址和ip地址。
      */
      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地址
      ip = iph->saddr;
      iph->saddr = iph->daddr;
      iph->daddr = ip;
    
      //重新计算ip头部信心中的tot_len,tot_len代表ip数据包的长度
      iph->tot_len = htons(ntohs(iph->tot_len) + extend_len);
    
      /*  交换udp端口 */
      port = udph->source;
      udph->source = udph->dest;
      udph->dest = port;
    
      //计算udp头部的长度
      udph->len = htons(ntohs(udph->len) + extend_len);
    
      //下面是根据wireshark抓包得到的
      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);
      
      //填充12位固定的数据,在dns应答报文的截图中可以看到
      memcpy(ptr, dns_answer, 12);
      //将br0的ip地址填充到skb中
      memcpy(ptr + 12, (unsigned char *)&br0_in_dev->ifa_list->ifa_address, 4);
    
      /* ip checksum */
      // TODO: CHECKSUM_NONE表示发送侧已经计算了校验和,协议栈将不会再计算校验和
      skb->ip_summed = CHECKSUM_NONE;
    
      //计算ip头部校验和,在计算之前应该先赋值为0
      iph->check = 0;
      iph->check = ip_fast_csum((unsigned char *)iph, iph->ihl);
    
      /* udp checksum */
      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;
    }
    
    /*
        该函数只会判断ipv4的报文,ipv6的报文这里不会涉及
    */
    int br_dns_trap_enter(struct sk_buff *skb) {
      struct iphdr *iph;
      struct udphdr *udph;
    
      iph = (struct iphdr *)skb_network_header(skb);
    
      //判断是否为ipv4的报文
      if (iph->version != 4) {
        return -1;
      }
      //获取udp头部
      udph = (void *)iph + iph->ihl * 4;
      // DNS报文是属于udp数据包,同时端口号为53
      if (iph->protocol != IPPROTO_UDP || ntohs(udph->dest) != 53) {
        return -1;
      }
      //获取DNS报文中的domain_name的长度
      /*
        说明:udp数据包头部信息中的len字段是udp头部信息和数据信息的总长度
        domain_name_len的长度和我们在应用层看到的domain_name的长度不一样,比如应用层我们看到的www.baidu.com他的长度为12个字节。
        但是在数据包中一般为14个字节,因为他会指明域名的长度(具体看wireshark抓包截图)
        ntohs(udph->len) - sizeof(struct udphdr) -
        sizeof(dnsheader_t)代表的是dns请求报文中的Queries字段的长度。该字段也是放在报文的最后面(我们请求的域名就包含在该字段中)
        那么这里为什么还要减去4呢,因Queries中有4个字节是固定的,分别为Type和Class字段,他们一共占据4个字节。减去4之后剩下的,才是我们真正的domain_name_len的长度
      */
      int domain_name_len =
          ntohs(udph->len) - sizeof(struct udphdr) - sizeof(dnsheader_t) - 4;
      if (domain_name_len <= 1) {
        return -1;
      }
      // 获取dns头部信息
      dnsheader_t *dns_hdr = (dnsheader_t *)((void *)udph + sizeof(struct udphdr));
    
      //判断dns头部是否有效
      if (is_valid_dns_query_header(dns_hdr) != true) {
        return -1;
      }
      //获取dns中的数据包
      unsigned char *body =
          (void *)udph + sizeof(struct udphdr) + sizeof(dnsheader_t);
    
      //获取dns数据包中的domain_len(这里是按照wireshark中来解析的)
      unsigned char domain_name[128] = {0};
      if (get_domain_name(body, domain_name, domain_name_len) != 0) {
        return -1;
      }
    
      //判断报文中获取到的domain_name和应用层传入的domain_name是否相等
      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交流

    展开全文
  • 下午使用qmail进行拦截测试的时候出了点问题,后来debug发现qmail竟然打了一条ANY类型的请求!原来还有这种类型,长见识了。 找到几个公用的DNS服务器,以后可以增加多服务器切换的功能。 多服务器的可用性是个...

    今天,太阳照常升起,逃过了一劫,那就开始新的生命吧。

    为了把BlackHole推广出去,想要做一个MacOS下的包。调研了mac开机启动的东西,将启动程序写成了一个plist文件,放在/Library/LaunchDaemons下面,脚本是这样的:

    <?xml version="1.0" encoding="utf-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
      <dict>
          <key>Label</key>
          <string>blackhole.init</string>
          <key>ProgramArguments</key>
          <array>
            <string>/usr/local/blackhole/blackhole-start.sh</string>
          </array>
            <key>KeepAlive</key>
            <false/>
          <key>RunAtLoad</key>
            <true/>
            <key>StandardErrorPath</key>
            <string>/tmp/blackhole.err</string>
            <key>StandardOutPath</key>
            <string>/tmp/blackhole.out</string>
            <key>UserName</key>
            <string>root</string>
      </dict>
    </plist>
    

    事实证明UserName=root那段必须加上,要不然不会用root权限启动,然后就会失败!

    重启后一切正常,开始使用BlackHole作为DNS服务器,考察稳定性。

    早上来到公司就出事了,主进程启动了,wifesays没启动,导致telnet无响应。悲剧!

    为什么wifesays老是启动不起来?

    后来知道,因为Spring初始化时是阻塞进行的,包括afterPropertySet方法,如果里面调用了耗时较长的程序,则会阻塞直到服务结束才会继续初始化,而这个过程的顺序是未知的。ehcache初始化的时间挺久(好多秒,不知道为什么),然后就导致wifesays的进程一直等不到Spring初始化结束,这时调用之,就抛出了空指针异常!后来将ehcache做了异步加载,并进行了错误判断,问题解决!

    下午使用qmail进行拦截测试的时候出了点问题,后来debug发现qmail竟然打了一条ANY类型的请求!原来还有这种类型,长见识了。

    找到几个公用的DNS服务器,以后可以增加多服务器切换的功能。

    多服务器的可用性是个问题,刚好最近也在研究服务器的可用性维护。最终实现如下:使用failedTimes和wakeup机制。failedTimes由外部调用指定,当次数过多时,标志为不可用;内部线程定期循环,尝试检查不可用的服务器,一旦可用,则重新标志为可用。

    转载于:https://my.oschina.net/flashsword/blog/97390

    展开全文
  • 拦截 DNS 解析,首先得找到系统哪个类去解析 Host 的。

    要拦截 DNS 解析,首先得找到系统哪个类去解析 Host 的,找到对应的 hook 点去实现。

    一,去了解一些常用的联网类:

    Volley 默认使用的是 HttpURLConnection。
    而 HttpURLConnection 底层 DNS 解析用的是:

    InetAddress.getAllByName();
    

    Okhttp 是可以自定义 DNS 的:

    new OkHttpClient().newBuilder().dns(new Dns() {
    	@Override
    	public List<InetAddress> lookup(String hostname) throws UnknownHostException {
    		.....................
    	}
    });
    

    而 Okhttp 默认 DNS 是 Dns.SYSTEM,实际也是 InetAddress.getAllByName();

    二,分析 InetAddress 源码,找到 Hook 点:

    InetAddress.getAllByName(); 在 Android 7.0 之后有修改。
    这里先分析 Android 7.0 之后的源码:

    public class InetAddress implements java.io.Serializable {
    	static final InetAddressImpl impl = new Inet6AddressImpl();
        public static InetAddress[] getAllByName(String host) throws UnknownHostException {
            return impl.lookupAllHostAddr(host, NETID_UNSET).clone();
        }
    }
    // 发现 Inet6AddressImpl 是个 接口实现类
    class Inet6AddressImpl implements InetAddressImpl {
    	.............
    }
    

    那么通过反射和动态代理,重新赋值 impl 变量,就可以拦截 DNS 解析了。

    再来分析 Android 7.0 之前的源码:

    public class InetAddress implements java.io.Serializable {
    	private static final AddressCache addressCache = new AddressCache();
        public static InetAddress[] getAllByName(String host) throws UnknownHostException {
            return getAllByNameImpl(host, NETID_UNSET).clone();
        }
    
        private static InetAddress[] getAllByNameImpl(String host, int netId) throws UnknownHostException {
            if (host == null || host.isEmpty()) {
                return loopbackAddresses();
            }
    
            // Is it a numeric address?
            InetAddress result = parseNumericAddressNoThrow(host);
            if (result != null) {
                result = disallowDeprecatedFormats(host, result);
                if (result == null) {
                    throw new UnknownHostException("Deprecated IPv4 address format: " + host);
                }
                return new InetAddress[] { result };
            }
    
            return lookupHostByName(host, netId).clone();
        }
    
    
        private static InetAddress[] lookupHostByName(String host, int netId) throws UnknownHostException {
        	// 这里会从 缓存中取
            Object cachedResult = addressCache.get(host, netId);
            if (cachedResult != null) {
                if (cachedResult instanceof InetAddress[]) {
                    return (InetAddress[]) cachedResult;
                }
            }
            .................
        }
    }
    
    class AddressCache {
        private static final int MAX_ENTRIES = 16;
        // cache just 2s.
        private static final long TTL_NANOS = 2 * 1000000000L;
        
        static class AddressCacheEntry {
            final Object value;
            final long expiryNanos;
            AddressCacheEntry(Object value) {
                this.value = value;
                this.expiryNanos = System.nanoTime() + TTL_NANOS;
            }
        }
    
    	// 这里 get 的时候,缓存有有效时间
    	// 而从 put 方法,AddressCacheEntry 构造方法里,知道有效时间是 TTL_NANOS(2s)
        public Object get(String hostname, int netId) {
            AddressCacheEntry entry = cache.get(new AddressCacheKey(hostname, netId));
            if (entry != null && entry.expiryNanos >= System.nanoTime()) {
                return entry.value;
            }
            return null;
        }
    
        public void put(String hostname, int netId, InetAddress[] addresses) {
            cache.put(new AddressCacheKey(hostname, netId), new AddressCacheEntry(addresses));
        }
    }
    

    从源码我们了解到可以利用 addressCache 缓存,提前处理好 DNS解析,但是这个缓存又只有 2s 有效时间。

    三,具体实现代码:

    public class DnsUtils {
        private final static String TAG = "DnsUtils";
        private static final Pattern IPV4_PATTERN = Pattern.compile("^(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}$");
    
        public static void hook() {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                hookN();
            }
        }
    
        public static boolean isIpv4(String address) {
            return IPV4_PATTERN.matcher(address).matches();
        }
    
        /**
         * 7.0 之后 InetAddress 才有 impl
         */
        private static void hookN() {
            try {
                if (LogUtils.isDebug()) {
                    LogUtils.d(TAG, "invoke 111:ipv6FirstN");
                }
                //获取InetAddress中的impl
                Field impl = InetAddress.class.getDeclaredField("impl");
                impl.setAccessible(true);
                //获取accessFlags
                Field modifiersField = Field.class.getDeclaredField("accessFlags");
                modifiersField.setAccessible(true);
                //去final
                modifiersField.setInt(impl, impl.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
                //获取原始InetAddressImpl对象
                final Object originalImpl = impl.get(null);
                //构建动态代理InetAddressImpl对象
                Object dynamicImpl = Proxy.newProxyInstance(originalImpl.getClass().getClassLoader(), originalImpl.getClass().getInterfaces(), new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        //如果函数名为lookupAllHostAddr,并且参数长度为2,第一个参数是host,第二个参数是netId
                        Object originalResult = method.invoke(originalImpl, args);
                        if (method.getName().equals("lookupAllHostAddr") && args != null && args.length == 2 && originalResult != null) {
                            InetAddress[] originalAddresses = (InetAddress[]) originalResult;
                            return myDnsLogic(originalAddresses);
                        }
                        return originalResult;
                    }
                });
                //替换impl为动态代理对象
                impl.set(null, dynamicImpl);
                //还原final
                modifiersField.setInt(impl, impl.getModifiers() & java.lang.reflect.Modifier.FINAL);
            } catch (Throwable e) {
                if (LogUtils.isDebug()) {
                    LogUtils.e(TAG, "invoke 555:" + e);
                }
            }
        }
    
        private static InetAddress[] myDnsLogic(InetAddress[] originalAddresses) {
            // 做自己的逻辑;
            return result;
        }
    
        /**
         * 7.0之前,不能动态代理,只能 addressCache 缓存来弄。
         * 而缓存只有 2S 有效时间
         */
        private static void hookM(final String hostName) {
            BackgroundExecutors.getGlobalExecutor().post(new Runnable() {
                @Override
                public void run() {
                    try {
                        if (LogUtils.isDebug()) {
                            LogUtils.d(TAG, "invoke 111:ipv6FirstM");
                        }
                        InetAddress[] originalAddresses = InetAddress.getAllByName(hostName);
                        InetAddress[] result = myDnsLogic(originalAddresses);
                        Class inetAddressClass = null;
                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                            inetAddressClass = Class.forName("java.net.Inet6AddressImpl");
                        } else {
                            inetAddressClass = InetAddress.class;
                        }
                        Field field = inetAddressClass.getDeclaredField("addressCache");
                        field.setAccessible(true);
                        Object object = field.get(inetAddressClass);
                        LogUtils.d(TAG, "test 11:" + object);
                        Class cacheClass = object.getClass();
                        Method putMethod;
                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                            //put方法在api21及以上为put(String host, int netId, InetAddress[] address)
                            putMethod = cacheClass.getDeclaredMethod("put", String.class, int.class, InetAddress[].class);
                            putMethod.setAccessible(true);
                            putMethod.invoke(object, hostName, 0, result);
                        } else {
                            //put方法在api20及以下为put(String host, InetAddress[] address)
                            putMethod = cacheClass.getDeclaredMethod("put", String.class, InetAddress[].class);
                            putMethod.setAccessible(true);
                            putMethod.invoke(object, hostName, result);
                        }
                        InetAddress[] test = InetAddress.getAllByName(hostName);
                        if (LogUtils.isDebug()) {
                            for (InetAddress inetAddress : test) {
                                LogUtils.d(TAG, "test 777:" + inetAddress);
                            }
                        }
                    } catch (Throwable e) {
                        if (LogUtils.isDebug()) {
                            LogUtils.e(TAG, "invoke 666:" + e);
                        }
                    }
                }
            });
        }
    }
    

    四,总结:

    1,如果项目中只使用了 Okhttp,那么可以自定义 DNS 解析,很简单。
    2,如果项目中用的其他库,所以可以参考上面的第三点的代码。
    3,利用反射拦截的方式,对于7.0以下的手机,不方便,只因为缓存只有2秒,所以必须得在每次请求前调用一次 hookM 方法。

    展开全文
  • dns域名拦截 具体描述:笔记本这边访问www.baidu.com,服务器将其拦截到自己的服务中,返回自定义内容。 防火墙开放53端口firewall-cmd --add-service=dns --permanent firewall-cmd --add-port=53/tcp --zone=...

    dns域名拦截

    具体描述:笔记本这边访问www.baidu.com,服务器将其拦截到自己的服务中,返回自定义内容。

    1. 防火墙开放53端口
      firewall-cmd --add-service=dns --permanent
      firewall-cmd --add-port=53/tcp --zone=public --permanent
      firewall-cmd --add-port=53/udp --zone=public --permanent
      firewall-cmd --reload
      systemctl restart firewalld
      
    2. 修改dnsmasq配置(没有安装的话可以通过yum安装)
      1. 修改/etc/dnsmasq.conf
      resolv-file=/etc/resolv.dnsmasq.conf    # dnsmasq 会从这个文件中寻找上游dns服务器
      strict-order            
      interface=eth1                        # 监听网卡eth1
      listen-address=0.0.0.0                # 监听所有地址
      address=/baidu.com/192.168.0.145      # 域名解析
      no-hosts                              # 取消解析默认的hosts文件
      
      1. 修改/etc/resolv.conf
      echo 'nameserver 127.0.0.1' > /etc/resolv.conf
      
      1. 创建resolv.dnsmasq.conf文件并添加上游dns服务器的地址
      touch /etc/resolv.dnsmasq.conf
      # 添加上游的DNS服务器,也就是公网DNS,这里添加114.114.114
      echo 'nameserver 114.114.114.114' > /etc/resolv.dnsmasq.conf
      
      
      1. 创建dnsmasq.hosts文件
      cp /etc/hosts /etc/dnsmasq.hosts
      # 在这个目里面添加记录
      echo 'addn-hosts=/etc/dnsmasq.hosts' >> /etc/dnsmasq.conf
      
    3. 修改内网主机DNS服务器为服务器eth1的ip

    shell脚本

    dns-cfg.sh

    #!/bin/sh
    # 配置dnsmasq的dns劫持,将内网主机的域名请求解析成指定的ip
    
    echo "# 防火墙配置"
    firewall-cmd --add-service=dns --permanent
    firewall-cmd --add-port=53/tcp --zone=public --permanent
    firewall-cmd --add-port=53/udp --zone=public --permanent
    firewall-cmd --reload
    systemctl restart firewalld
    
    echo "# 修改dnsmasq配置"
    # dnsmasq 会从这个文件中寻找上游dns服务器, 默认是从hosts文件中寻找
    echo 'resolv-file=/etc/resolv.dnsmasq.conf' >> /etc/dnsmasq.conf
    # 表示严格按照resolv-file文件中的顺序从上到下进行DNS解析,直到第一个解析成功为止
    echo 'strict-order' >> /etc/dnsmasq.conf
    # 监听网卡eth1
    echo 'interface=eth1' >> /etc/dnsmasq.conf
    # 域名解析
    ip=$(awk -F "=" '/^IPADDR/{print $2}' /etc/sysconfig/network-scripts/ifcfg-eth0)
    echo "address=/baidu.com/${ip}" >> /etc/dnsmasq.conf
    # 取消解析默认的hosts文件
    echo 'no-hosts' >> /etc/dnsmasq.conf
    # 修改/etc/resolv.conf
    echo 'nameserver 127.0.0.1' > /etc/resolv.conf
    # 创建resolv.dnsmasq.conf文件并添加上游dns服务器的地址
    touch /etc/resolv.dnsmasq.conf
    echo 'nameserver 114.114.114.114' > /etc/resolv.dnsmasq.conf
    
    # 创建dnsmasq.hosts文件
    cp /etc/hosts /etc/dnsmasq.hosts
    echo 'addn-hosts=/etc/dnsmasq.hosts' >> /etc/dnsmasq.conf
    
    echo "# 启动dnsmasq"
    systemctl enable dnsmasq
    systemctl start dnsmasq
    
    展开全文
  • 路由器 设置DNS 域名拦截 重定向

    千次阅读 2017-02-13 13:24:03
    而实际情况,手里的路由器并不支持域名重定向,因此第二种方案,搭建一个测试用DNS服务器test_DNS,将test_DNS加入到路由器的DNS服务器列表,这个搭建的test_DNS中将weather.sophia.com的域名重定向到具体ip地址。...
  • AdGuard Home 的工作原理是在 DNS 的域名解析过程中拦截网页上的广告。AdGuard Home 支持 DNS over TLS 和 DNS over HTTPS。本文讲解在OpenWRT配置AdGuardHome,实现DNS防污染加快网站解析速度和广告拦截。 一、...
  • block-this, 阻止基于这里免费DNS的广告拦截器 阻止这里- 一个基于DNS的Android广告阻止程序 版权所有( C ) 2015 -2017 Sava Georgiev 。 这个程序是免费软件。你可以在GNU通用 public 许可证版本 3.0中重新发布它并...
  • DNS 解析器模块可保护用户免受 DNS 拦截和配置更新攻击,并改进了 DNS 解析的网络性能。此模块包含用于实现 DNS 桩解析器的代码,该解析器可将www.google.com等名称转换为 IP 地址(例如2001:db8::1)。DNS 桩解析器...
  • DNS-Based Host Blocking for Android This is a DNS-based host blocker for Android. In the default configuration, several widely-respected host files are used to block ads, malware, and other weird ...
  • 这里以我自己的IP地址为例,192.168.xx.154是我的Mac mini的内网IP,这样就可以实现同一个局域网内的设备,通过设置DNS为这个IP,来实现都通过我的Mac mini上的dnsmasq来查询dns,即局域网范围内的DNS泛解析。...
  • 原文: ... 安装AdGuard Home wget ... ... xvf AdGuardHome_linux_amd64.tar.gz ...安装完成后可以在浏览器中输入以下内容,在端口3000(防火墙需要开通对应的端口)上访问AdGuard ...本站自建DNS服务器 94.191.120.231
  • 基于以上原因,一般Web服务器都绑定8080端口,手机浏览器如果输入IP地址,会访问Web服务器的80端口,这样就需要进行端口报文转发,对应dns报文拦截,无法监听53端口,同样需要端口转发,此外,浏览器的搜索引擎如果...
  • 然而,这种信任可能被 隐藏的DNS解析路径的拦截(我们称之为DNS拦截)所破坏。具体来说,沿路的设备可能冒充用户指定的DNS服务器的IP地址,并暗中拦截DNS查询,从而带来隐私和安全问题。 在本文中,我们进行了大...
  • 今天突然看到原来大名鼎鼎的AdGuard开源了一个AdGuard Home,原理就是在dns查询的层面就拦截了广告域名,达到不加载广告就拦截广告的目的,而且因为是dns,所以只需要给设备配置dns就可以,简单方便。由于是dns,...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 423
精华内容 169
关键字:

dns拦截