GoでのDNSプロキシの作成







広告に関する問題を解決したいとずっと思っていました。 すべてのデバイスでこれを行う最も簡単な方法は、広告ドメインのIPアドレスに対するブロック要求でDNSサーバーを上げることでした。



私が最初に始めたのはdnsmasqを使用することでしたが、インターネットからリストをダウンロードして使用統計を取得したいと考えました。 そのため、サーバーを作成することにしました。



もちろん、完全にゼロから書かれているわけではなく、DNSのすべての作業はこのライブラリから取得されます



構成



もちろん、構成ファイルをロードすることにより、プログラムは動作を開始します。 すぐに、サーバーの再起動を回避するために、構成が変更されたときに自動的に構成をロードする必要性について考えました。 このため、fsnotifyパッケージが役立ちました。



構成構造:



type Config struct { Nameservers []string `yaml:"nameservers"` Blocklist []string `yaml:"blocklist"` BlockAddress4 string `yaml:"blockAddress4"` BlockAddress6 string `yaml:"blockAddress6"` ConfigUpdate bool `yaml:"configUpdate"` UpdateInterval time.Duration `yaml:"updateInterval"` }
      
      





ここで最も興味深い点は、構成ファイルの更新を追跡することです。 ライブラリを使用して、これは非常に簡単に行われます。Watcherを作成し、ファイルをフックし、チャンネルからのイベントをリッスンします。 本当だ!



コード
 func configWatcher() { watcher, err := fsnotify.NewWatcher() if err != nil { log.Fatal(err) } defer watcher.Close() err = watcher.Add(*configFile) if err != nil { log.Fatal(err) } for { select { case event := <-watcher.Events: if event.Op&fsnotify.Write == fsnotify.Write { log.Println("Config file updated, reload config") c, err := loadConfig() if err != nil { log.Println("Bad config: ", err) } else { log.Println("Config successfuly updated") config = c if !c.ConfigUpdate { return } } } case err := <-watcher.Errors: log.Println("error:", err) } } }
      
      







ブラックリスト



もちろん、目的は不要なサイトをブロックすることなので、それらはどこかに保存する必要があります。 これを行うには、小さな負荷で、ロックされたドメインがキーとして使用される空の構造の単純なハッシュテーブルが適しています。 最後にポイントを持つ必要があることに注意してください。

しかし、同時に読み取り/書き込みができないため、ミューテックスなしで実行できます。



コード
 type BlackList struct { data map[string]struct{} } func (b *BlackList) Add(server string) bool { server = strings.Trim(server, " ") if len(server) == 0 { return false } if !strings.HasSuffix(server, ".") { server += "." } b.data[server] = struct{}{} return true } func (b *BlackList) Contains(server string) bool { _, ok := b.data[server] return ok }
      
      







キャッシング



当初、私はそれなしでもできると思っていました。結局のところ、すべてのデバイスが大量のリクエストを作成するわけではありません。 しかし、ある夕方、私のサーバーが何らかの形で発見され、約100 rpsの頻度で同じリクエストでサーバーをフラッディングし始めました。 はい、これは大したことではありませんが、リクエストは実際のネームスペースサーバー(私の場合はGoogle)にプロキシされ、ロックを取得することは非常に不快です。



キャッシングの主な問題は、多数の異なるリクエストであり、それらを個別に保存する必要があるため、2レベルのハッシュテーブルがあります。



コード
 type Cache interface { Get(reqType uint16, domain string) dns.RR Set(reqType uint16, domain string, ip dns.RR) } type CacheItem struct { Ip dns.RR Die time.Time } type MemoryCache struct { cache map[uint16]map[string]*CacheItem locker sync.RWMutex } func (c *MemoryCache) Get(reqType uint16, domain string) dns.RR { c.locker.RLock() defer c.locker.RUnlock() if m, ok := c.cache[reqType]; ok { if ip, ok := m[domain]; ok { if ip.Die.After(time.Now()) { return ip.Ip } } } return nil } func (c *MemoryCache) Set(reqType uint16, domain string, ip dns.RR) { c.locker.Lock() defer c.locker.Unlock() var m map[string]*CacheItem m, ok := c.cache[reqType] if !ok { m = make(map[string]*CacheItem) c.cache[reqType] = m } m[domain] = &CacheItem{ Ip: ip, Die: time.Now().Add(time.Duration(ip.Header().Ttl) * time.Second), } }
      
      







ハンドラー



もちろん、プログラムの主要部分は着信リクエストハンドラであるため、デザート用に残しました。 基本的なロジックは次のようなものです。リクエストを受信し、ブラックリストでその存在を確認し、キャッシュでその存在を確認し、実サーバーにリクエストをプロキシ化します。



主な関心は、ルックアップの機能です。 その中で、一度にすべてのサーバーに要求を同時に送信し(応答が到着する前に時間があれば)、少なくとも1つのサーバーからの応答が成功するのを待ちます。



コード
 func Lookup(req *dns.Msg) (*dns.Msg, error) { c := &dns.Client{ Net: "tcp", ReadTimeout: time.Second * 5, WriteTimeout: time.Second * 5, } qName := req.Question[0].Name res := make(chan *dns.Msg, 1) var wg sync.WaitGroup L := func(nameserver string) { defer wg.Done() r, _, err := c.Exchange(req, nameserver) totalRequestsToGoogle.Inc() if err != nil { log.Printf("%s socket error on %s", qName, nameserver) log.Printf("error:%s", err.Error()) return } if r != nil && r.Rcode != dns.RcodeSuccess { if r.Rcode == dns.RcodeServerFailure { return } } select { case res <- r: default: } } ticker := time.NewTicker(5 * time.Second) defer ticker.Stop() // Start lookup on each nameserver top-down, in every second for _, nameserver := range config.Nameservers { wg.Add(1) go L(nameserver) // but exit early, if we have an answer select { case r := <-res: return r, nil case <-ticker.C: continue } } // wait for all the namservers to finish wg.Wait() select { case r := <-res: return r, nil default: return nil, errors.New("can't resolve ip for" + qName) } }
      
      







指標



メトリックには、プロメテウスのクライアントを使用します。 これは非常に簡単に使用されます。最初にカウンターを宣言し、それを登録して、適切な場所でInc()メソッドを呼び出す必要があります。 主なことは、メトリックを読み取ることができるように、プロメテウスハンドラーでWebサーバーを起動することを忘れないことです。



コード
 var ( totalRequestsTcp = prometheus.NewCounter(prometheus.CounterOpts(prometheus.Opts{ Namespace: "dns", Subsystem: "requests", Name: "total", Help: "total requests", ConstLabels: map[string]string{ "type": "tcp", }, })) ) func runPrometheus() { prometheus.MustRegister(totalRequestsTcp) http.Handle("/metrics", promhttp.Handler()) log.Fatal(http.ListenAndServe(":9970", nil)) }
      
      







mainにはプレゼンテーションや説明は必要ないと思います。 この記事では、コードを短縮形式で示しています。



完全なコードはリポジトリで見ることができます (もちろん修正や追加は大歓迎です)。 リポジトリには、Docker用のファイルとGitlab用のサンプルCI構成もあります。



ご清聴ありがとうございました。



All Articles