SouLogic 灵魂逻辑

针对指定域名的 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 会及时发现连接问题并重连。