Skip to content

Instantly share code, notes, and snippets.

@zhengkai
Last active January 28, 2021 03:55
Show Gist options
  • Save zhengkai/f5e7ca58da1db542d2fae9d0bc82034d to your computer and use it in GitHub Desktop.
Save zhengkai/f5e7ca58da1db542d2fae9d0bc82034d to your computer and use it in GitHub Desktop.
针对指定域名的 TCP 流量分析方法

针对指定域名的 TCP 流量分析方法

需求

需要对某手机 app 做分析和诊断,该 app 会 TCP 长连接远程多台主机(不同域名)中的一个。

大体方式

建立一个特定端口范围的代理服务器(这里用 golang 写的),使用 DNS server(这里使用 coredns) 将指定范围的域名指向代理 IP,其他一切照常,这样手机端只要更改 DNS 即可切换是否走代理,这样影响最小。

因为通讯内容本身没有包含目标地址的信息(类似 HTTP 中的 host),所以需要 coredns 通过 dnstap 协议的插件将相关查询的域名发过来,代理通过来源 IP 做匹配。

具体实现

首先是 coredns,只需要很少的几行就可以配置,这里 192.168.1.1 是上游 DNS 地址,app-server.com 是要更改的域名,127.0.0.1 为变化后的解析

(proxy) {
    template IN A {
        answer "{{ .Name }} 60 IN A 127.0.0.1"
    }
    log
}

app-server.com {
    import proxy
}

. {
    forward . 192.168.1.1:53
    cache 10800
    log
}

这里是泛域名解析(如 foo.bar.app-server.com 也同样会指到 127.0.0.1

注意 DNS server 要独占 53 端口(理论上可使用任何端口,但通常的网络设置里不会有端口选项),我的解决方式是用虚拟机另占一个 IP 地址,当然相应的更改后 IP 也要做相应更改。

启动后可以通过 dig 命令确认 DNS 是否生效,如

dig foo.bar.app-server.com @127.0.0.1

启用 dnstap,只需要简单的在配置开头 (proxy) { 段里增加一行

dnstap tcp://127.0.0.1:6000 full

这样所有查询 log 会通过长连接实时汇报给代理服务器。相应的,代理服务器需要监听 6000 端口。这里讲下 golang 里的具体实现。

通过 net.Listen("tcp", 6000) 并轮询 Accept() 会获得 dnstap 连过来的 net.Conn,因为是 TCP 长连接,所以需要解决基本的分帧,在看了 dnstap 的源代码后得知使用的是 golang-framestream,分帧后,实际通讯用的是 protobuf,dnstap 提供了对应的 .proto 文件 反序列化。这部分具体代码如下

func dnstapConn(c net.Conn) {

    defer c.Close()

    reader, err := framestream.NewReader(c,
        &framestream.ReaderOptions{
            ContentTypes:  [][]byte{[]byte("protobuf:dnstap.Dnstap")},
            Bidirectional: true,
        })

    if err != nil {
        return
    }


    ab := make([]byte, 8192)
    for {

        n, err := reader.ReadFrame(ab)
        if err != nil {
            break
        }

        d := &dnstap.Dnstap{}
        err = proto.Unmarshal(ab[:n], d)
        if err != nil {
            continue
        }

需要解释一下,d := &dnstap.Dnstap{} 是我将其 .proto 文件加了一行 option go_package = ".;dnstap"; 并编译为 dnstap/dnstap.pb.go 文件,以上操作获取了完整的信息,之后具体调用就很简单了

        msg := d.GetMessage()
        if msg.GetType() != dnstap.Message_CLIENT_QUERY {
            continue
        }

        dm := new(dns.Msg)
        err = dm.Unpack(msg.QueryMessage)
        if err != nil || len(dm.Question) < 1 {
            continue
        }

        domain := dm.Question[0].Name
        clientIP := net.IP(msg.QueryAddress).String()

这里的 dns 实际是用的 miekg/dns。最终得到了想要的两个变量 domainclientIP

之后就是根据 DNS 查询结果和实际对代理的连接做匹配后就可以连接目标服务器了。

其实最初是考虑解析 coredns 的文本 log,但实际搞明白 dnstap 用的几个库之后,这种方式从效率上更好,代码也更简练(不需要处理很多关于文本操作的异常)。

另外需要注意,如果代理服务器重启后,coredns 并不会马上重连,解决方式是代理启动后就立即执行三次针对要代理的域名的 dig 操作,这样 coredns 会及时发现连接问题并重连。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment