From bb75c421a6d0b2c3129cde4069df4821ad54c131 Mon Sep 17 00:00:00 2001 From: Adam Date: Sat, 30 May 2020 00:36:23 -0500 Subject: [PATCH] initial commit --- .gitignore | 6 ++ Dockerfile | 18 +++++ blocklists.go | 55 +++++++++++++++ blocklists_test.go | 99 ++++++++++++++++++++++++++ config.go | 31 ++++++++ config.json | 21 ++++++ go.mod | 5 ++ go.sum | 19 +++++ main.go | 172 +++++++++++++++++++++++++++++++++++++++++++++ makefile | 19 +++++ 10 files changed, 445 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 blocklists.go create mode 100644 blocklists_test.go create mode 100644 config.go create mode 100644 config.json create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 makefile diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ca56cf8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.bin +.vscode + +gopherhole +debug.test +__debug_bin \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7383da6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM golang AS build + + +WORKDIR /opt/build + +COPY . /opt/build + +RUN go get -v && go build -v -o gopherhole ./main.go + +FROM ubuntu + +COPY --from=build /opt/build/gopherhole /opt/gopherhole/gopherhole + +WORKDIR /opt/gopherhole + +EXPOSE 53 + +ENTRYPOINT /opt/gopherhole/gopherhole \ No newline at end of file diff --git a/blocklists.go b/blocklists.go new file mode 100644 index 0000000..4b7877c --- /dev/null +++ b/blocklists.go @@ -0,0 +1,55 @@ +package main + +import ( + "bufio" + "fmt" + "net" + "net/http" + "strings" +) + +type Blocklist struct { + Source string + Domains map[string]string +} + +func FetchBlockList(source string) (Blocklist, error) { + blocklist := Blocklist{ + Source: source, + Domains: map[string]string{}, + } + + response, err := http.Get(source) + if err != nil { + return blocklist, err + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return blocklist, fmt.Errorf("non 200 status: %d", response.StatusCode) + } + + scanner := bufio.NewScanner(response.Body) + for scanner.Scan() { + line := scanner.Text() + if line == "" || line[0] == '#' { + continue + } + + domain := line + if frags := strings.Split(line, " "); len(frags) > 1 { + if net.ParseIP(frags[1]) != nil { + continue + } + domain = frags[1] + } + + if strings.Contains(domain, "localhost") || strings.Contains(domain, "loopback") || domain == "broadcasthost" { + continue + } + + blocklist.Domains[domain] = "enabled" + } + + return blocklist, nil +} diff --git a/blocklists_test.go b/blocklists_test.go new file mode 100644 index 0000000..c220860 --- /dev/null +++ b/blocklists_test.go @@ -0,0 +1,99 @@ +package main + +import ( + "reflect" + "testing" +) + +func TestFetchBlockList(t *testing.T) { + type args struct { + source string + } + tests := []struct { + name string + source string + want Blocklist + wantErr bool + }{ + { + name: "test complex block list", + source: "https://gist.githubusercontent.com/adamveld12/7d7a236344c48d6e00cc4bd0d88079bf/raw/93b837ff443d1e4c4c9a381275733fd28d16b5f7/blacklist.txt", + want: Blocklist{ + Source: "https://gist.githubusercontent.com/adamveld12/7d7a236344c48d6e00cc4bd0d88079bf/raw/93b837ff443d1e4c4c9a381275733fd28d16b5f7/blacklist.txt", + Domains: []string{ + "local", + "ip6-localnet", + "ip6-mcastprefix", + "ip6-allnodes", + "ip6-allrouters", + "ip6-allhosts", + "01mspmd5yalky8.com", + "0byv9mgbn0.com", + "analytics.247sports.com", + "www.analytics.247sports.com", + "2no.co", + "www.2no.co", + "logitechlogitechglobal.112.2o7.net", + "www.logitechlogitechglobal.112.2o7.net", + "30-day-change.com", + "www.30-day-change.com", + }, + }, + }, + { + name: "test simple block list", + source: "https://s3.amazonaws.com/lists.disconnect.me/simple_tracking.txt", + want: Blocklist{ + Source: "https://s3.amazonaws.com/lists.disconnect.me/simple_tracking.txt", + Domains: []string{ + "adjust.io", + "airbrake.io", + "appboy.com", + "appsflyer.com", + "apsalar.com", + "bango.combango.org", + "bango.net", + "basic-check.disconnect.me", + "bkrtx.com", + "bluekai.com", + "bugsense.com", + "burstly.com", + "chartboost.com", + "count.ly", + "crashlytics.com", + "crittercism.com", + "custom-blacklisted-tracking-example.com", + "do-not-tracker.org", + "eviltracker.net", + "flurry.com", + "getexceptional.com", + "inmobi.com", + "jumptap.com", + "localytics.com", + "mixpanel.com", + "mobile-collector.newrelic.com", + "mobileapptracking.com", + "playtomic.com", + "stathat.com", + "supercell.net", + "tapjoy.com", + "trackersimulator.org", + "usergrid.com", + "vungle.com", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := FetchBlockList(tt.source) + if (err != nil) != tt.wantErr { + t.Errorf("FetchBlockList() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("FetchBlockList() = %+v, want %+v", got, tt.want) + } + }) + } +} diff --git a/config.go b/config.go new file mode 100644 index 0000000..0f81ba3 --- /dev/null +++ b/config.go @@ -0,0 +1,31 @@ +package main + +import ( + "encoding/json" + "io/ioutil" +) + +func LoadConfig(path string) (*Configuration, error) { + data, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + + var c Configuration + if err := json.Unmarshal(data, &c); err != nil { + return nil, err + } + + return &c, nil +} + +type Configuration struct { + Upstream []string + Blocklists []string + Records map[string]ConfigRecord +} + +type ConfigRecord struct { + Type string + Record string +} diff --git a/config.json b/config.json new file mode 100644 index 0000000..b8e9145 --- /dev/null +++ b/config.json @@ -0,0 +1,21 @@ +{ + "upstream": [ + "1.1.1.1", + "8.8.8.8" + ], + "records": { + "vdhsn.com": { + "Type": "A", + "Record": "192.168.0.4" + }, + "riffraff.vdhsn.com": { + "Type": "A", + "Record": "192.168.0.4" + } + }, + "blocklists": [ + "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts", + "https://s3.amazonaws.com/lists.disconnect.me/simple_ad.txt", + "https://s3.amazonaws.com/lists.disconnect.me/simple_tracking.txt" + ] +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..cb35a10 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/adamveld12/gohperhole + +go 1.14 + +require github.com/miekg/dns v1.1.29 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..cdd9906 --- /dev/null +++ b/go.sum @@ -0,0 +1,19 @@ +github.com/miekg/dns v1.1.29 h1:xHBEhR+t5RzcFJjBLJlax2daXOrTYtr9z4WdKEfWFzg= +github.com/miekg/dns v1.1.29/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7MvYXnkV9/iQNo1lX6g= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe h1:6fAMxZRR6sl1Uq8U61gxU+kPTs2tR8uOySCbBP7BN/M= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/main.go b/main.go new file mode 100644 index 0000000..c744f2e --- /dev/null +++ b/main.go @@ -0,0 +1,172 @@ +package main + +import ( + "errors" + "fmt" + "log" + "net" + "time" + + "github.com/miekg/dns" +) + +func main() { + fmt.Println("hello") + + cfg, err := LoadConfig("./config.json") + if err != nil { + log.Fatal(err) + } + + blockLists := []Blocklist{} + for _, bl := range cfg.Blocklists { + blocklist, err := FetchBlockList(bl) + if err != nil { + continue + } + + log.Printf("block list '%s' -> %d", bl, len(blocklist.Domains)) + blockLists = append(blockLists, blocklist) + } + + srv := &dns.Server{ + Addr: ":53", + Net: "udp", + Handler: &handler{ + Config: *cfg, + Cache: DNSCache(map[string]DNSCacheRecord{}), + BlockLists: blockLists, + }, + } + + if err := srv.ListenAndServe(); err != nil { + log.Fatal(err) + } +} + +type handler struct { + Config Configuration + BlockLists []Blocklist + Cache DNSCache +} + +type DNSCacheRecord struct { + Expiration time.Time + IP net.IP +} + +type DNSCache map[string]DNSCacheRecord + +func (d DNSCache) Get(domain string, msg *dns.Msg) bool { + if record, ok := d[domain]; ok && time.Now().UTC().Before(record.Expiration) { + msg.Answer = append(msg.Answer, &dns.A{ + Hdr: dns.RR_Header{Name: domain, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 60}, + A: record.IP, + }) + return true + } + return false +} + +func (d DNSCache) Set(domain string, IP net.IP, ttl time.Duration) { + if IP != nil && ttl > 0 { + log.Printf("setting %s to cache", domain) + d[domain] = DNSCacheRecord{ + Expiration: time.Now().UTC().Add(ttl), + IP: IP, + } + } else { + delete(d, domain) + } +} + +func (h *handler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { + msg := dns.Msg{} + msg.SetReply(r) + + switch r.Question[0].Qtype { + case dns.TypeA: + msg.Authoritative = true + domain := msg.Question[0].Name + fmt.Println("got request", domain) + + if address, ok := h.Config.Records[domain[:len(domain)-1]]; ok && address.Type == "A" { + msg.Answer = append(msg.Answer, &dns.A{ + Hdr: dns.RR_Header{Name: domain, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 60}, + A: net.ParseIP(address.Record), + }) + } else if list, block := shouldBlock(h.BlockLists, domain); block { + log.Printf("blocked dns query for '%s' from '%s'", domain, list) + msg.Answer = append(msg.Answer, &dns.A{ + Hdr: dns.RR_Header{Name: domain, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 60}, + A: net.ParseIP("127.0.0.1"), + }) + } else if !h.Cache.Get(domain, &msg) { + msg, ip, ttl, err := recursiveResolve(domain, h.Config.Upstream...) + if err != nil { + log.Printf("got an error trying to resolve '%s': %v", domain, err) + break + } + h.Cache.Set(domain, ip, ttl) + msg.SetReply(r) + w.WriteMsg(msg) + return + } + } + + w.WriteMsg(&msg) +} + +func shouldBlock(bls []Blocklist, domain string) (string, bool) { + for _, b := range bls { + if _, ok := b.Domains[domain[:len(domain)-1]]; ok { + return b.Source, true + } + } + + return "", false +} + +func recursiveResolve(domain string, servers ...string) (*dns.Msg, net.IP, time.Duration, error) { + c := dns.Client{ + DialTimeout: time.Second, + ReadTimeout: time.Second, + } + + for _, server := range servers { + m := dns.Msg{} + m.SetQuestion(domain, dns.TypeA) + m.Compress = true + r, t, err := c.Exchange(&m, server+":53") + if err != nil { + return nil, nil, 0, err + } + + log.Printf("Took %v", t) + if len(r.Answer) == 0 { + continue + } + + res := dns.Msg{} + for _, ans := range r.Answer { + switch ans.(type) { + case *dns.A: + Arecord := ans.(*dns.A) + res.Answer = append(res.Answer, &dns.A{ + Hdr: dns.RR_Header{Name: domain, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 60}, + A: Arecord.A, + }) + return &res, Arecord.A, time.Second * time.Duration(Arecord.Hdr.Ttl), nil + case *dns.CNAME: + Crecord := ans.(*dns.CNAME) + res.Answer = append(res.Answer, &dns.CNAME{ + Hdr: dns.RR_Header{Name: domain, Rrtype: dns.TypeCNAME, Class: dns.ClassINET, Ttl: 60}, + Target: Crecord.Target, + }) + return &res, nil, 0, nil + } + } + } + + return nil, nil, 0, errors.New("no record found") +} diff --git a/makefile b/makefile new file mode 100644 index 0000000..2ce10d4 --- /dev/null +++ b/makefile @@ -0,0 +1,19 @@ +APP := gopherhole +BIN := .bin + +dev: clean $(BIN)/config.json $(BIN)/$(APP) + sudo $(BIN)/$(APP) + +$(BIN)/$(APP): + go build -v -o $(BIN)/$(APP) . + +$(BIN)/config.json: $(BIN) + @cp ./config.json $(BIN)/config.json + +$(BIN): + mkdir -p $@ + +clean: + @rm -rf $(BIN) + +.PHONY: dev clean \ No newline at end of file