DNS 代理?Pipy:這我也可以

Pipy 是個可編程代理,曾經我們做過 TCP/HTTP 代理MQTT 代理Dubbo 代理Redis 代理Thrift 代理。前幾天有人問 DNS[1] 的代理能不能做?當然可以,而且 DNS 代理已經應用在 跨集羣流量調度 中,文末經對此進行簡單地介紹。

閱讀本文將瞭解到:

DNS 介紹

DNS(Domain Name System,域名系統)是互聯網的一項服務。它將域名和 IP 地址相互映射爲一個分佈式數據庫,能夠使人更方便地訪問互聯網。DNS 使用 TCP 和 UDP 端口 53。

-- 摘自維基百科

dns procedure

簡化版的 DNS 處理流程:

  1. DNS 客戶端(如瀏覽器、應用程序或者設備)發送域名 example.com 的查詢請求。

  2. DNS 解析器收到請求,查詢本地緩存,如果本地有記錄且未過期會返回本地的記錄。

  3. 如果本地緩存未命中,DNS 解析器將從 DNS 根服務器開始向下查詢,首先是頂級域名(Top Level Domain, TLD) DNS 服務器(這裏是 .com),一直向下直到可以解析 example.com 的服務器。

  4. 能夠解析 example.com 的服務器成爲權威 DNS 名稱服務器(Authoritative DNS name server),解析器訪問該服務器並收到 IP 地址等相關信息,然後返回給給客戶端。解析完成。

相信在工作的時候會遇到需要改 DNS 記錄來更新域名的真實指向,比如切換運行環境、流量攔截,DNS 也經常作爲服務發現的手段之一。通常 DNS 服務器要麼是服務提供商業維護,要麼就是企業內部的網絡團隊,導致修改 DNS 的解析記錄不夠便利。而且由於 DNS 的緩存設計,每條記錄都有個 TTL 的設置,在緩存失效前都不會再去更新記錄。TTL 過長過短,都不合適。

引入 DNS 代理,可以在解決這個問題的同時,實現更多的功能。

接下來通過案例來演示如何使用 Pipy 實現 DNS 的代理(準確來講,應該是代理和服務器的合體),這個代理會從自定義的記錄中返回 DNS 查詢請求。同時我們還會加入特性:根據客戶端 IP 的地址返回不同的 DNS 記錄,來實現智能線路解析。演示中所使用的腳本,都可以從 這裏 [2] 下載。

方案

dns-proxy

如上圖所示,DNS 代理與原來的解析器,提供類似的功能。但是在緩存失效或者未命中時,會查詢自定義的解析記錄。如果有自定義記錄,就返回自定義記錄;如果沒有,按照原來的流程去 DNS 服務器上查詢。

實現

在開始之前,藉助 wireshark 的網絡抓包來看下 DNS 消息的格式,DNS 查詢和應答的消息格式是一樣的,都包含一下四個部分:

dns-message-format

在 Pipy 0.70.0 的更新 中,假如了 DNS 的解碼器。使用 DNS 解碼器,可以對 DNS 消息進行解碼,解碼出上面的四個部分。

PipJS 編碼

實現的腳本邏輯很簡單,爲了方便閱讀將其按功能分成了幾個模塊,實現了 AAAAACNAMEMXTXTNS 幾個常見類型的記錄解析。

├── cache.js #緩存
├── main.js #主入口腳本
├── records.js #自定義記錄的邏輯
├── records.json #自定義記錄的內容
├── smart-line.js #智能線路解析的邏輯
└── smart-line.json #智能線路解析的配置

這裏列出 main.js[3] 的部分核心代碼,並對代碼進行了註解:

  1. 首先使用 DNS.decode() 對數據流進行解碼

  2. 然後從結果中找到要查詢域名和類型

  3. 查詢緩存

  4. 緩存未命中,查詢自定義的記錄。

  5. 智能線路解析

  6. 返回響應

  7. 如果 3、4 均查詢不到,會請求上游的 DNS 服務器,然後緩存並返回響應

.listen(5300, { protocol: 'udp' })
.replaceMessage(
  msg => (
    (query, res, record) => (
      query = DNS.decode(msg.body), //1
      query?.question?.[0]?.name && query?.question?.[0]?.type && ( //2
        record = getDNS(query.question[0].type + '#' + query.question[0].name) //3
        || local.query(query.question[0].name, query.question[0].type) //4
      ),
      record ? (
        record = line.filter(__inbound.remoteAddress, record), //5
        res = {},
        res.qr = res.rd = res.ra = res.aa = 1,
        res.id = query.id,
        res.question = [{
          'name': query.question[0].name,
          'type': query.question[0].type
        }],
        record.status === 'deny' ? (
          res.rcode = local.code.REFUSED
        ) : (
          res.answer = record.rr
        ),
        new Message(DNS.encode(res)) //6
      ) : (
        _forward = true,
        msg
      )
    )
  )()
)
.branch(
  () => _forward, $ => $
    .connect(() => `${config.upstreamDNSServer}:53`, { protocol: 'udp' }) //7
    .handleMessage(
      msg => (
        (res = DNS.decode(msg.body)) => (
          res?.question?.[0]?.name && res?.question?.[0]?.type &&
          !res?.rcode && (
            setDNS(res.question[0].type + '#' + res.question[0].name,
              {
                rr: res.answer,
                status: res.rcode == local.code.REFUSED ? 'deny' : null
              }
            )
          )
        )
      )()
    ),
  $ => $
)

自定義記錄

下面是自定義記錄的內容,與 DNS 應答的格式類似。爲了支持智能線路解析,部分記錄增加了標籤信息:"labels": ["line1"]

[
  {
    "name": "example.com",
    "type": "A",
    "ttl": 60,
    "rdata": "192.168.139.10",
    "labels": ["line1"]
  },
  {
    "name": "example.com",
    "type": "A",
    "ttl": 60,
    "rdata": "192.168.139.11",
    "labels": ["line2"]
  },
  ...
  {
    "name": "example.com",
    "type": "MX",
    "ttl": 600,
    "rdata": {
      "preference": 10,
      "exchange": "mail2.example.com"
    }
  },
  {
    "name": "example.com",
    "type": "TXT",
    "ttl": 600,
    "rdata": "hi.pipy!"
  },
  {
    "name": "example.com",
    "type": "NS",
    "ttl": 600,
    "rdata": "ns1.example.com"
  },
  ...
]

智能線路解析

智能線路解析的邏輯比較簡單,爲不同的 IP 範圍設置線路標籤,在應答時如果記錄帶有標籤就只返回對應標籤的記錄。

{
  "line1": [
    "192.168.1.110/32"
  ],
  "line2": [
    "127.0.0.1/32"
  ]
}

測試

啓動代理:

$ pipy main.js

如上面配置所示,127.0.0.1 是本機迴環網卡的地址,192.168.1.110 本機以太網卡的地址,代理監聽在 5300 端口。

首先使用 localhost 訪問代理,這樣代理獲取的客戶端 IP 地址爲 127.0.0.1,在查詢 example.com 的記錄時,直返回來地址對應的線路 line2 的記錄 192.168.139.11

$ dig @localhost -p 5300 a example.com

; <<>> DiG 9.10.6 <<>> @localhost -p 5300 a example.com
; (2 servers found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 25868
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;example.com.   IN A

;; ANSWER SECTION:
example.com.  60 IN A 192.168.139.11

;; Query time: 0 msec
;; SERVER: 127.0.0.1#5300(127.0.0.1)
;; WHEN: Tue Dec 13 21:09:38 CST 2022
;; MSG SIZE  rcvd: 56

接着使用 192.168.1.110 訪問代理,這次客戶端的地址爲 192.168.1.110,返回的是線路 line1 的記錄 192.168.139.10

$ dig @192.168.1.110 -p 5300 a example.com

; <<>> DiG 9.10.6 <<>> @192.168.1.110 -p 5300 a example.com
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 54165
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;example.com.   IN A

;; ANSWER SECTION:
example.com.  60 IN A 192.168.139.10

;; Query time: 0 msec
;; SERVER: 192.168.1.110#5300(192.168.1.110)
;; WHEN: Tue Dec 13 21:12:37 CST 2022
;; MSG SIZE  rcvd: 56

假如我從另外一臺機器上訪問,因爲沒有設置線路,會返回兩條記錄。

$ dig @192.168.1.110 -p 5300 a example.com

; <<>> DiG 9.16.1-Ubuntu <<>> @192.168.1.110 -p 5300 a example.com
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 64873
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;example.com.   IN A

;; ANSWER SECTION:
example.com.  60 IN A 192.168.139.10
example.com.  60 IN A 192.168.139.11

;; Query time: 0 msec
;; SERVER: 192.168.1.110#5300(192.168.1.110)
;; WHEN: Tue Dec 13 13:15:24 UTC 2022
;; MSG SIZE  rcvd: 83

因爲只設置了 A 記錄的線路,其他類型的記錄不受影響。

$dig @localhost -p 5300 mx example.com

; <<>> DiG 9.10.6 <<>> @localhost -p 5300 mx example.com
; (2 servers found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 33492
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;example.com.   IN MX

;; ANSWER SECTION:
example.com.  600 IN MX 10 mail1.example.com.
example.com.  600 IN MX 10 mail2.example.com.

;; Query time: 0 msec
;; SERVER: 127.0.0.1#5300(127.0.0.1)
;; WHEN: Tue Dec 13 21:18:27 CST 2022
;; MSG SIZE  rcvd: 117

進階

對 Pipy 有一定了解的小夥伴可能知道 Repo 模式 [4],有興趣的可以參考這篇文章 快速入門 Pipy Repo(文章一年前發佈,界面和 API 接口有更新,但是原理不變)。

使用 Repo 模式,所有主機上的代理(或者稱之爲 DNS 服務器)都從 Repo 中實時獲取自定義記錄的更新,並刷新緩存。

礙於篇幅,這裏就不深入。有興趣的小夥伴可以嘗試自己實現。

dynamic-dns-resolve

總結

至此 Pipy 可以實現的代理又增加了一種。DNS 的應用無處不在,也正因如此從 DNS 層面可以解決問題。

讓我們再回到開頭提到的問題,在 跨集羣流量調度實戰 的 demo 中,我們將輕鬆將請求流量調度到了其他集羣進行處理。請求的地址是 http://httpbin.httpbin:8080/,這裏的 httpbin.httbin 是命名空間 httpbin 下 K8s Service httpbin 的域名。但是在集羣 cluster-2 中並沒有這個 Service,僅在集羣 cluster-1 和 cluster-3 中部署,這個地址在集羣 cluster-2 中無法解析。

這裏使用了個小手段,在網格的初始化容器設置 iptables 規則攔截流量時,也 DNS 的流量也攔截到 sidecar 實現的 DNS 代理(監聽在 127.0.0.153:5300),通過自定義 DNS 記錄實現業務流量的攔截。

fsm-multi-cluster

引用鏈接

[1] DNS: https://en.wikipedia.org/wiki/Domain_Name_System
[2] 這裏: https://github.com/flomesh-io/pipy-demos/tree/main/pipy-dns-demo
[3] main.jshttps://github.com/flomesh-io/pipy-demos/blob/main/pipy-dns-demo/main.js
[4] Repo 模式: https://flomesh.io/pipy/docs/en/operating/repo/0-intro

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/eZgRwaZrp5m-Ut51oXoSSQ