7 Commits

Author SHA1 Message Date
c0d5ef7e22 change workdir to config dir 2021-01-08 19:12:58 +01:00
fe7c207065 implemented lego HTTP endpoint 2021-01-08 16:08:57 +01:00
c7bac27a53 nxdomain if no rr found 2021-01-06 23:06:47 +01:00
6d49db2b6b added comments 2021-01-06 15:53:58 +01:00
7f40a04638 only one question per query 2020-12-31 14:11:07 +01:00
a71b619763 updated README 2020-12-30 22:18:08 +01:00
9828429bea added DNS over TLS 2020-12-30 21:59:33 +01:00
5 changed files with 269 additions and 54 deletions

View File

@@ -23,10 +23,23 @@ acl: # List of ip filter rules
- name: local
cidr: 127.0.0.1/32
tls:
enable: true # Enable DNS over TLS
address: 0.0.0.0:8853 # What address and port to liste for tls connections
cert: cert.crt # Path to the certificate file
key: private.key # Path to the private key file
forward:
acl: # What IPs are allowed
- vpn
server: "8.8.8.8:53" # DNS server to forward to
address: 0.0.0.0:8053 # What address and port to listen on
blacklist: # What domains to block when forwarding
# URL of the blacklist
- url: https://raw.githubusercontent.com/anudeepND/blacklist/master/adservers.txt
format: host # Format of the blacklist: Hostfile
- url: https://blocklistproject.github.io/Lists/alt-version/ads-nl.txt
format: line # Format: One domain per line
```

View File

@@ -69,6 +69,7 @@ func getBlacklistFromURL(url string) (*string, error) {
return &bodyString, err
}
// parseRawBlacklist parse the raw string depending on the given format
func parseRawBlacklist(blacklist configBlacklist, raw string) []string {
switch blacklist.Format {
case "host":
@@ -82,6 +83,7 @@ func parseRawBlacklist(blacklist configBlacklist, raw string) []string {
}
}
// parseHostFormat parse the string in the format of a hostfile
func parseHostFormat(raw string) []string {
finalList := make([]string, 0)
reg := regexp.MustCompile(`(?mi)^\s*(#*)\s*(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\s+([a-zA-Z0-9\.\- ]+)$`)
@@ -95,6 +97,7 @@ func parseHostFormat(raw string) []string {
return finalList
}
// parseLineFormat one domain per line, ignore comments
func parseLineFormat(raw string) []string {
list := make([]string, 0)
@@ -113,6 +116,7 @@ func handleBlockedDomain(w dns.ResponseWriter, r *dns.Msg) {
m := new(dns.Msg)
m.SetReply(r)
if q.Qtype == dns.TypeA {
// Respond with 0.0.0.0
m.Answer = append(m.Answer, &dns.A{
Hdr: dns.RR_Header{
Name: q.Name,
@@ -123,6 +127,7 @@ func handleBlockedDomain(w dns.ResponseWriter, r *dns.Msg) {
A: nullIPv4,
})
} else if q.Qtype == dns.TypeAAAA {
// Respond with ::/0
m.Answer = append(m.Answer, &dns.AAAA{
Hdr: dns.RR_Header{
Name: q.Name,

View File

@@ -22,8 +22,20 @@ forward:
address: 0.0.0.0:8053
tls:
enable: true
address: 0.0.0.0:8853
cert: cert.crt
key: private.key
blacklist:
- url: https://raw.githubusercontent.com/anudeepND/blacklist/master/adservers.txt
format: host
- url: https://blocklistproject.github.io/Lists/alt-version/ads-nl.txt
format: line
lego:
enable: true
address: :8080
username: lego
secret: "133742069ab"

View File

@@ -7,6 +7,7 @@ import (
"net"
"os"
"os/signal"
"path/filepath"
"strings"
"syscall"
@@ -23,12 +24,15 @@ type zoneMap map[string][]zoneView
type rrMap map[uint16]map[string][]dns.RR
// config format of the config file
type config struct {
Zones []configZone `yaml:"zones"`
ACL []configACL `yaml:"acl"`
Forward configForward `yaml:"forward"`
Address string `yaml:"address"`
Blacklist []configBlacklist `yaml:"blacklist"`
TLS configTLS `yaml:"tls"`
Lego configLego `yaml:"lego"`
}
type configForward struct {
@@ -52,6 +56,14 @@ type configBlacklist struct {
Format string `yaml:"format"`
}
type configTLS struct {
Enable bool `yaml:"enable"`
Address string `yaml:"address"`
Cert string `yaml:"cert"`
Key string `yaml:"key"`
}
// All record types to send when a ANY request is send
var anyRecordTypes = []uint16{
dns.TypeSOA,
dns.TypeA,
@@ -101,6 +113,7 @@ func loadZones(configZones []configZone) (zoneMap, error) {
return zones, nil
}
// createRRMap order the rr into a structure that is more easy to use
func createRRMap(rrs []dns.RR) rrMap {
rrMap := make(rrMap)
for _, rr := range rrs {
@@ -139,6 +152,7 @@ func loadZonefile(filepath, origin string) ([]dns.RR, error) {
return rrs, nil
}
// createACLList create a map with the CIDR and the name of the rule
func createACLList(config []configACL) (map[string]*net.IPNet, error) {
acls := make(map[string]*net.IPNet)
@@ -155,10 +169,12 @@ func createACLList(config []configACL) (map[string]*net.IPNet, error) {
return acls, nil
}
func createServer(zones zoneMap, config config, aclList map[string]*net.IPNet, blacklist map[string]bool) *dns.ServeMux {
// createServer creates a new serve mux. Adds all the logic to handle the request
func createServer(zones zoneMap, config config, aclList map[string]*net.IPNet, blacklist map[string]bool, acmeList *legoMap) *dns.ServeMux {
srv := dns.NewServeMux()
c := new(dns.Client)
// For all zones set from the config
for zoneName, zones := range zones {
srv.HandleFunc(zoneName, func(w dns.ResponseWriter, r *dns.Msg) {
@@ -170,6 +186,11 @@ func createServer(zones zoneMap, config config, aclList map[string]*net.IPNet, b
return
}
// Check if it is a ACME DNS-01 challange
if handleACMERequest(w, r, acmeList) {
return
}
// find out what view to handle the request
zoneIndex := -1
@@ -179,6 +200,7 @@ func createServer(zones zoneMap, config config, aclList map[string]*net.IPNet, b
}
}
// No view found that can handle the request
if zoneIndex == -1 {
rcodeRequest(w, r, dns.RcodeRefused)
return
@@ -188,8 +210,10 @@ func createServer(zones zoneMap, config config, aclList map[string]*net.IPNet, b
})
}
// Handle any other request
// Handle any other request for forwarding
srv.HandleFunc(".", func(w dns.ResponseWriter, r *dns.Msg) {
// Parse IP
remoteIP, _, err := net.SplitHostPort(w.RemoteAddr().String())
ip := net.ParseIP(remoteIP)
@@ -198,12 +222,18 @@ func createServer(zones zoneMap, config config, aclList map[string]*net.IPNet, b
return
}
// Check if it is a ACME DNS-01 challange
if handleACMERequest(w, r, acmeList) {
return
}
// Check ACL rules
if !checkACL(config.Forward.ACL, aclList, ip) {
rcodeRequest(w, r, dns.RcodeRefused)
return
}
// Check if the domain is bocked
if _, ok := blacklist[r.Question[0].Name]; ok {
handleBlockedDomain(w, r)
} else {
@@ -214,21 +244,23 @@ func createServer(zones zoneMap, config config, aclList map[string]*net.IPNet, b
rcodeRequest(w, r, dns.RcodeServerFailure)
return
}
w.WriteMsg(in)
}
})
return srv
}
func listenAndServer(server *dns.ServeMux, address string) {
// Start UDP listner
go func() {
if err := dns.ListenAndServe(address, "udp", server); err != nil {
log.Fatalf("Failed to set udp listener %s\n", err.Error())
}
}()
// Start TCP listner
go func() {
if err := dns.ListenAndServe(address, "tcp", server); err != nil {
log.Fatalf("Failed to set tcp listener %s\n", err.Error())
@@ -236,6 +268,15 @@ func listenAndServer(server *dns.ServeMux, address string) {
}()
}
func listenAndServerTLS(server *dns.ServeMux, address, cert, key string) {
// Start TLS listner
go func() {
if err := dns.ListenAndServeTLS(address, cert, key, server); err != nil {
log.Fatalf("Failed to set DoT listener %s", err.Error())
}
}()
}
func checkACL(alcRules []string, aclList map[string]*net.IPNet, ip net.IP) bool {
if len(alcRules) != 0 {
passed := false
@@ -250,6 +291,7 @@ func checkACL(alcRules []string, aclList map[string]*net.IPNet, ip net.IP) bool
return true
}
// rcodeRequest respond to a request with a response code
func rcodeRequest(w dns.ResponseWriter, r *dns.Msg, rcode int) {
m := new(dns.Msg)
m.SetReply(r)
@@ -257,13 +299,19 @@ func rcodeRequest(w dns.ResponseWriter, r *dns.Msg, rcode int) {
w.WriteMsg(m)
}
// handleRequest find the right RR(s) in the view and send them back
func handleRequest(w dns.ResponseWriter, r *dns.Msg, zone zoneView) {
m := new(dns.Msg)
m.SetReply(r)
m.Authoritative = true
// maybe only support one question per query like most servers do it ???
for _, q := range r.Question {
// Only support one question per query because all the other server also does that
if len(r.Question) != 1 {
rcodeRequest(w, r, dns.RcodeServerFailure)
}
q := r.Question[0]
rrs := zone.rr[q.Qtype]
// Handle ANY
@@ -323,6 +371,9 @@ func handleRequest(w dns.ResponseWriter, r *dns.Msg, zone zoneView) {
}
}
}
if len(m.Answer) == 0 {
m.SetRcode(m, dns.RcodeNameError)
}
w.WriteMsg(m)
@@ -333,6 +384,11 @@ func main() {
configPath := flag.String("c", "/etc/cool-dns/config.yaml", "path to the config file")
flag.Parse()
err := os.Chdir(filepath.Dir(*configPath))
if err != nil {
log.Fatalf("Failed to goto config dir: %s", err.Error())
}
config, err := loadConfig(*configPath)
if err != nil {
log.Fatalf("Failed to load config: %s\n", err.Error())
@@ -350,10 +406,21 @@ func main() {
blacklist := loadBlacklist(config.Blacklist)
server := createServer(zones, *config, aclList, blacklist)
var acmeMap *legoMap
if config.Lego.Enable {
acmeMap = startLEGOWebSever(config.Lego)
}
server := createServer(zones, *config, aclList, blacklist, acmeMap)
listenAndServer(server, config.Address)
if config.TLS.Enable {
listenAndServerTLS(server, config.TLS.Address, config.TLS.Cert, config.TLS.Key)
log.Printf("Start listening on tcp %s for tls", config.TLS.Address)
}
log.Printf("Start listening on udp %s and tcp %s\n", config.Address, config.Address)
sig := make(chan os.Signal)

118
lego.go Normal file
View File

@@ -0,0 +1,118 @@
package main
import (
"encoding/json"
"log"
"net/http"
"github.com/miekg/dns"
)
// See https://go-acme.github.io/lego/dns/httpreq/
type configLego struct {
Enable bool `yaml:"enable"`
Address string `yaml:"address"`
Username string `yaml:"username"`
Secret string `yaml:"secret"`
}
type legoPresent struct {
Fqdn string `json:"fqdn"`
Value string `json:"value"`
}
type legoMap map[string]string
func startLEGOWebSever(config configLego) *legoMap {
mux := http.NewServeMux()
acmeMap := make(legoMap)
mux.HandleFunc("/present", func(rw http.ResponseWriter, r *http.Request) {
if !checkBasicAuth(r, config) {
rw.WriteHeader(http.StatusUnauthorized)
return
}
var presentData legoPresent
err := json.NewDecoder(r.Body).Decode(&presentData)
defer r.Body.Close()
if err != nil {
log.Printf("Failed to parse request for ACME: %s", err.Error())
}
acmeMap[presentData.Fqdn] = presentData.Value
rw.WriteHeader(http.StatusOK)
})
mux.HandleFunc("/cleanup", func(rw http.ResponseWriter, r *http.Request) {
if !checkBasicAuth(r, config) {
rw.WriteHeader(http.StatusUnauthorized)
return
}
for k := range acmeMap {
delete(acmeMap, k)
}
rw.WriteHeader(http.StatusOK)
})
go func() {
if err := http.ListenAndServe(config.Address, mux); err != nil {
log.Fatalf("Failed to start Webserver for LEGO: %s\n", err.Error())
}
}()
log.Printf("Startet webserver on %s", config.Address)
return &acmeMap
}
func checkBasicAuth(r *http.Request, config configLego) bool {
if config.Username != "" && config.Secret != "" {
u, p, ok := r.BasicAuth()
if !ok {
return false
}
if u == config.Username && p == config.Secret {
return true
}
log.Printf("Failed lego authentication")
return false
}
return true
}
func handleACMERequest(w dns.ResponseWriter, r *dns.Msg, acmeMap *legoMap) bool {
if len(r.Question) == 1 {
if r.Question[0].Qtype == dns.TypeTXT && r.Question[0].Qclass == dns.ClassINET {
if value, ok := (*acmeMap)[r.Question[0].Name]; ok {
m := new(dns.Msg)
m.SetReply(r)
m.Answer = append(m.Answer, &dns.TXT{
Hdr: dns.RR_Header{
Name: r.Question[0].Name,
Rrtype: dns.TypeTXT,
Class: dns.ClassINET,
Ttl: 0,
},
Txt: []string{value},
})
w.WriteMsg(m)
return true
}
}
}
return false
}