initial commit

master
Adam Veldhousen 4 years ago committed by Adam Veldhousen
commit bb75c421a6
Signed by: adam
GPG Key ID: 6DB29003C6DD1E4B

6
.gitignore vendored

@ -0,0 +1,6 @@
.bin
.vscode
gopherhole
debug.test
__debug_bin

@ -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

@ -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
}

@ -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)
}
})
}
}

@ -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
}

@ -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"
]
}

@ -0,0 +1,5 @@
module github.com/adamveld12/gohperhole
go 1.14
require github.com/miekg/dns v1.1.29

@ -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=

@ -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")
}

@ -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
Loading…
Cancel
Save