commit
bb75c421a6
@ -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…
Reference in new issue