Initial commit after restructure

This commit is contained in:
Daniel
2018-08-13 14:14:27 +02:00
commit bdeddc41f9
177 changed files with 26108 additions and 0 deletions

62
intel/data.go Normal file
View File

@@ -0,0 +1,62 @@
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
package intel
import (
"github.com/Safing/safing-core/database"
datastore "github.com/ipfs/go-datastore"
)
// EntityClassification holds classification information about an internet entity.
type EntityClassification struct {
lists []byte
}
// Intel holds intelligence data for a domain.
type Intel struct {
database.Base
Domain string
DomainOwner string
CertOwner string
Classification *EntityClassification
}
var intelModel *Intel // only use this as parameter for database.EnsureModel-like functions
func init() {
database.RegisterModel(intelModel, func() database.Model { return new(Intel) })
}
// Create saves the Intel with the provided name in the default namespace.
func (m *Intel) Create(name string) error {
return m.CreateObject(&database.IntelCache, name, m)
}
// CreateInNamespace saves the Intel with the provided name in the provided namespace.
func (m *Intel) CreateInNamespace(namespace *datastore.Key, name string) error {
return m.CreateObject(namespace, name, m)
}
// Save saves the Intel.
func (m *Intel) Save() error {
return m.SaveObject(m)
}
// getIntel fetches the Intel with the provided name in the default namespace.
func getIntel(name string) (*Intel, error) {
return getIntelFromNamespace(&database.IntelCache, name)
}
// getIntelFromNamespace fetches the Intel with the provided name in the provided namespace.
func getIntelFromNamespace(namespace *datastore.Key, name string) (*Intel, error) {
object, err := database.GetAndEnsureModel(namespace, name, intelModel)
if err != nil {
return nil, err
}
model, ok := object.(*Intel)
if !ok {
return nil, database.NewMismatchError(object, intelModel)
}
return model, nil
}

218
intel/dns.go Normal file
View File

@@ -0,0 +1,218 @@
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
package intel
import (
"fmt"
"net"
"time"
"github.com/Safing/safing-core/database"
datastore "github.com/ipfs/go-datastore"
"github.com/miekg/dns"
)
// RRCache is used to cache DNS data
type RRCache struct {
Answer []dns.RR
Ns []dns.RR
Extra []dns.RR
Expires int64
Modified int64
servedFromCache bool
requestingNew bool
}
func (m *RRCache) Clean(minExpires uint32) {
var lowestTTL uint32 = 0xFFFFFFFF
var header *dns.RR_Header
// set TTLs to 17
// TODO: double append? is there something more elegant?
for _, rr := range append(m.Answer, append(m.Ns, m.Extra...)...) {
header = rr.Header()
if lowestTTL > header.Ttl {
lowestTTL = header.Ttl
}
header.Ttl = 17
}
// TTL must be at least minExpires
if lowestTTL < minExpires {
lowestTTL = minExpires
}
m.Expires = time.Now().Unix() + int64(lowestTTL)
m.Modified = time.Now().Unix()
}
func (m *RRCache) ExportAllARecords() (ips []net.IP) {
for _, rr := range m.Answer {
if rr.Header().Class == dns.ClassINET && rr.Header().Rrtype == dns.TypeA {
aRecord, ok := rr.(*dns.A)
if ok {
ips = append(ips, aRecord.A)
}
} else if rr.Header().Class == dns.ClassINET && rr.Header().Rrtype == dns.TypeAAAA {
aRecord, ok := rr.(*dns.AAAA)
if ok {
ips = append(ips, aRecord.AAAA)
}
}
}
return
}
func (m *RRCache) ToRRSave() *RRSave {
var s RRSave
s.Expires = m.Expires
s.Modified = m.Modified
for _, entry := range m.Answer {
s.Answer = append(s.Answer, entry.String())
}
for _, entry := range m.Ns {
s.Ns = append(s.Ns, entry.String())
}
for _, entry := range m.Extra {
s.Extra = append(s.Extra, entry.String())
}
return &s
}
func (m *RRCache) Create(name string) error {
s := m.ToRRSave()
return s.CreateObject(&database.DNSCache, name, s)
}
func (m *RRCache) CreateWithType(name string, qtype dns.Type) error {
s := m.ToRRSave()
return s.Create(fmt.Sprintf("%s%s", name, qtype.String()))
}
func (m *RRCache) Save() error {
s := m.ToRRSave()
return s.SaveObject(s)
}
func GetRRCache(domain string, qtype dns.Type) (*RRCache, error) {
return GetRRCacheFromNamespace(&database.DNSCache, domain, qtype)
}
func GetRRCacheFromNamespace(namespace *datastore.Key, domain string, qtype dns.Type) (*RRCache, error) {
var m RRCache
rrSave, err := GetRRSaveFromNamespace(namespace, domain, qtype)
if err != nil {
return nil, err
}
m.Expires = rrSave.Expires
m.Modified = rrSave.Modified
for _, entry := range rrSave.Answer {
rr, err := dns.NewRR(entry)
if err == nil {
m.Answer = append(m.Answer, rr)
}
}
for _, entry := range rrSave.Ns {
rr, err := dns.NewRR(entry)
if err == nil {
m.Ns = append(m.Ns, rr)
}
}
for _, entry := range rrSave.Extra {
rr, err := dns.NewRR(entry)
if err == nil {
m.Extra = append(m.Extra, rr)
}
}
m.servedFromCache = true
return &m, nil
}
// ServedFromCache marks the RRCache as served from cache.
func (m *RRCache) ServedFromCache() bool {
return m.servedFromCache
}
// RequestingNew informs that it has expired and new RRs are being fetched.
func (m *RRCache) RequestingNew() bool {
return m.requestingNew
}
// Flags formats ServedFromCache and RequestingNew to a condensed, flag-like format.
func (m *RRCache) Flags() string {
switch {
case m.servedFromCache && m.requestingNew:
return " [CR]"
case m.servedFromCache:
return " [C]"
case m.requestingNew:
return " [R]" // theoretically impossible, but let's leave it here, just in case
default:
return ""
}
}
// IsNXDomain returnes whether the result is nxdomain.
func (m *RRCache) IsNXDomain() bool {
return len(m.Answer) == 0
}
// RRSave is helper struct to RRCache to better save data to the database.
type RRSave struct {
database.Base
Answer []string
Ns []string
Extra []string
Expires int64
Modified int64
}
var rrSaveModel *RRSave // only use this as parameter for database.EnsureModel-like functions
func init() {
database.RegisterModel(rrSaveModel, func() database.Model { return new(RRSave) })
}
// Create saves RRSave with the provided name in the default namespace.
func (m *RRSave) Create(name string) error {
return m.CreateObject(&database.DNSCache, name, m)
}
// CreateWithType saves RRSave with the provided name and type in the default namespace.
func (m *RRSave) CreateWithType(name string, qtype dns.Type) error {
return m.Create(fmt.Sprintf("%s%s", name, qtype.String()))
}
// CreateInNamespace saves RRSave with the provided name in the provided namespace.
func (m *RRSave) CreateInNamespace(namespace *datastore.Key, name string) error {
return m.CreateObject(namespace, name, m)
}
// Save saves RRSave.
func (m *RRSave) Save() error {
return m.SaveObject(m)
}
// GetRRSave fetches RRSave with the provided name in the default namespace.
func GetRRSave(name string, qtype dns.Type) (*RRSave, error) {
return GetRRSaveFromNamespace(&database.DNSCache, name, qtype)
}
// GetRRSaveFromNamespace fetches RRSave with the provided name in the provided namespace.
func GetRRSaveFromNamespace(namespace *datastore.Key, name string, qtype dns.Type) (*RRSave, error) {
object, err := database.GetAndEnsureModel(namespace, fmt.Sprintf("%s%s", name, qtype.String()), rrSaveModel)
if err != nil {
return nil, err
}
model, ok := object.(*RRSave)
if !ok {
return nil, database.NewMismatchError(object, rrSaveModel)
}
return model, nil
}

32
intel/doc.go Normal file
View File

@@ -0,0 +1,32 @@
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
/*
Package intel is responsible for fetching intelligence data, including DNS, on remote entities.
DNS Servers
Internal lists of resolvers to use are built on start and rebuilt on every config or network change.
Configured DNS servers are prioritized over servers assigned by dhcp. Domain and search options (here referred to as "search scopes") are being considered.
Security
Usage of DNS Servers can be regulated using the configuration:
DoNotUseAssignedDNS // Do not use DNS servers assigned by DHCP
DoNotUseMDNS // Do not use mDNS
DoNotForwardSpecialDomains // Do not forward special domains to local resolvers, except if they have a search scope for it
Note: The DHCP options "domain" and "search" are ignored for servers assigned by DHCP that do not reside within local address space.
Resolving DNS
Various different queries require the resolver to behave in different manner:
Queries for "localhost." are immediately responded with 127.0.0.1 and ::1, for A and AAAA queries and NXDomain for others.
Reverse lookups on local address ranges (10/8, 172.16/12, 192.168/16, fe80::/7) will be tried against every local resolver and finally mDNS until a successful, non-NXDomain answer is received.
Special domains ("example.", "example.com.", "example.net.", "example.org.", "invalid.", "test.", "onion.") are resolved using search scopes and local resolvers.
All other domains are resolved using search scopes and all available resolvers.
*/
package intel

48
intel/domainfronting.go Normal file
View File

@@ -0,0 +1,48 @@
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
package intel
import (
"github.com/Safing/safing-core/log"
"sync"
"github.com/miekg/dns"
)
var (
dfMap = make(map[string]string)
dfMapLock sync.RWMutex
)
func checkDomainFronting(hidden string, qtype dns.Type, securityLevel int8) (*RRCache, bool) {
dfMapLock.RLock()
front, ok := dfMap[hidden]
dfMapLock.RUnlock()
if !ok {
return nil, false
}
log.Tracef("intel: applying domain fronting %s -> %s", hidden, front)
// get domain name
rrCache := resolveAndCache(front, qtype, securityLevel)
if rrCache == nil {
return nil, true
}
// replace domain name
var header *dns.RR_Header
for _, rr := range rrCache.Answer {
header = rr.Header()
if header.Name == front {
header.Name = hidden
}
}
// save under front
rrCache.CreateWithType(hidden, qtype)
return rrCache, true
}
func addDomainFronting(hidden string, front string) {
dfMapLock.Lock()
dfMap[hidden] = front
dfMapLock.Unlock()
return
}

46
intel/intel.go Normal file
View File

@@ -0,0 +1,46 @@
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
package intel
import (
"github.com/Safing/safing-core/database"
"github.com/Safing/safing-core/modules"
"github.com/miekg/dns"
)
var (
intelModule *modules.Module
)
func init() {
intelModule = modules.Register("Intel", 128)
go Start()
}
// GetIntel returns an Intel object of the given domain. The returned Intel object MUST not be modified.
func GetIntel(domain string) *Intel {
fqdn := dns.Fqdn(domain)
intel, err := getIntel(fqdn)
if err != nil {
if err == database.ErrNotFound {
intel = &Intel{Domain: fqdn}
intel.Create(fqdn)
} else {
return nil
}
}
return intel
}
func GetIntelAndRRs(domain string, qtype dns.Type, securityLevel int8) (intel *Intel, rrs *RRCache) {
intel = GetIntel(domain)
rrs = Resolve(domain, qtype, securityLevel)
return
}
func Start() {
// mocking until intel has its own goroutines
defer intelModule.StopComplete()
<-intelModule.Stop
}

61
intel/ipinfo.go Normal file
View File

@@ -0,0 +1,61 @@
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
package intel
import (
"strings"
"github.com/Safing/safing-core/database"
datastore "github.com/ipfs/go-datastore"
)
// IPInfo represents various information about an IP.
type IPInfo struct {
database.Base
Domains []string
}
var ipInfoModel *IPInfo // only use this as parameter for database.EnsureModel-like functions
func init() {
database.RegisterModel(ipInfoModel, func() database.Model { return new(IPInfo) })
}
// Create saves the IPInfo with the provided name in the default namespace.
func (m *IPInfo) Create(name string) error {
return m.CreateObject(&database.IPInfoCache, name, m)
}
// CreateInNamespace saves the IPInfo with the provided name in the provided namespace.
func (m *IPInfo) CreateInNamespace(namespace *datastore.Key, name string) error {
return m.CreateObject(namespace, name, m)
}
// Save saves the IPInfo.
func (m *IPInfo) Save() error {
return m.SaveObject(m)
}
// GetIPInfo fetches the IPInfo with the provided name in the default namespace.
func GetIPInfo(name string) (*IPInfo, error) {
return GetIPInfoFromNamespace(&database.IPInfoCache, name)
}
// GetIPInfoFromNamespace fetches the IPInfo with the provided name in the provided namespace.
func GetIPInfoFromNamespace(namespace *datastore.Key, name string) (*IPInfo, error) {
object, err := database.GetAndEnsureModel(namespace, name, ipInfoModel)
if err != nil {
return nil, err
}
model, ok := object.(*IPInfo)
if !ok {
return nil, database.NewMismatchError(object, ipInfoModel)
}
return model, nil
}
// FmtDomains returns a string consisting of the domains that have seen to use this IP, joined by " or "
func (m *IPInfo) FmtDomains() string {
return strings.Join(m.Domains, " or ")
}

331
intel/mdns.go Normal file
View File

@@ -0,0 +1,331 @@
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
package intel
import (
"errors"
"fmt"
"net"
"github.com/Safing/safing-core/log"
"strings"
"sync"
"time"
"github.com/miekg/dns"
)
const (
DNSClassMulticast = dns.ClassINET | 1<<15
)
var (
multicast4Conn *net.UDPConn
multicast6Conn *net.UDPConn
unicast4Conn *net.UDPConn
unicast6Conn *net.UDPConn
questions = make(map[uint16]savedQuestion)
questionsLock sync.Mutex
)
type savedQuestion struct {
question dns.Question
expires int64
}
func init() {
go listenToMDNS()
}
func indexOfRR(entry *dns.RR_Header, list *[]dns.RR) int {
for k, v := range *list {
if entry.Name == v.Header().Name && entry.Rrtype == v.Header().Rrtype {
return k
}
}
return -1
}
func listenToMDNS() {
var err error
messages := make(chan *dns.Msg)
multicast4Conn, err = net.ListenMulticastUDP("udp4", nil, &net.UDPAddr{IP: net.IPv4(224, 0, 0, 251), Port: 5353})
if err != nil {
// TODO: retry after some time
log.Warningf("intel(mdns): failed to create udp4 listen multicast socket: %s", err)
} else {
go listenForDNSPackets(multicast4Conn, messages)
}
multicast6Conn, err = net.ListenMulticastUDP("udp6", nil, &net.UDPAddr{IP: net.IP([]byte{0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfb}), Port: 5353})
if err != nil {
// TODO: retry after some time
log.Warningf("intel(mdns): failed to create udp6 listen multicast socket: %s", err)
} else {
go listenForDNSPackets(multicast6Conn, messages)
}
unicast4Conn, err = net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4zero, Port: 0})
if err != nil {
// TODO: retry after some time
log.Warningf("intel(mdns): failed to create udp4 listen socket: %s", err)
} else {
go listenForDNSPackets(unicast4Conn, messages)
}
unicast6Conn, err = net.ListenUDP("udp6", &net.UDPAddr{IP: net.IPv6zero, Port: 0})
if err != nil {
// TODO: retry after some time
log.Warningf("intel(mdns): failed to create udp6 listen socket: %s", err)
} else {
go listenForDNSPackets(unicast6Conn, messages)
}
for {
select {
case message := <-messages:
// log.Tracef("intel: got net mdns message: %s", message)
var question *dns.Question
var saveFullRequest bool
scavengedRecords := make(map[string]*dns.RR)
var rrCache *RRCache
// save every received response
// if previous save was less than 2 seconds ago, add to response, else replace
// pick out A and AAAA records and save seperately
// continue if not response
if !message.Response {
// log.Tracef("intel: mdns message has no response, ignoring")
continue
}
// continue if rcode is not success
if message.Rcode != dns.RcodeSuccess {
// log.Tracef("intel: mdns message has error, ignoring")
continue
}
// continue if answer section is empty
if len(message.Answer) == 0 {
// log.Tracef("intel: mdns message has no answers, ignoring")
continue
}
// continue if no question
if len(message.Question) == 0 {
questionsLock.Lock()
savedQ, ok := questions[message.MsgHdr.Id]
questionsLock.Unlock()
if ok {
question = &savedQ.question
}
} else {
question = &message.Question[0]
}
if question != nil {
// continue if class is not INTERNET
if question.Qclass != dns.ClassINET && question.Qclass != DNSClassMulticast {
// log.Tracef("intel: mdns question is not of class INET, ignoring")
continue
}
saveFullRequest = true
}
// get entry from database
if saveFullRequest {
rrCache, err = GetRRCache(question.Name, dns.Type(question.Qtype))
if err != nil || rrCache.Modified < time.Now().Add(-2*time.Second).Unix() || rrCache.Expires < time.Now().Unix() {
rrCache = &RRCache{}
}
}
for _, entry := range message.Answer {
if strings.HasSuffix(entry.Header().Name, ".local.") || domainInScopes(entry.Header().Name, localReverseScopes) {
if saveFullRequest {
k := indexOfRR(entry.Header(), &rrCache.Answer)
if k == -1 {
rrCache.Answer = append(rrCache.Answer, entry)
} else {
rrCache.Answer[k] = entry
}
}
switch entry.(type) {
case *dns.A:
scavengedRecords[fmt.Sprintf("%sA", entry.Header().Name)] = &entry
case *dns.AAAA:
scavengedRecords[fmt.Sprintf("%sAAAA", entry.Header().Name)] = &entry
case *dns.PTR:
if !strings.HasPrefix(entry.Header().Name, "_") {
scavengedRecords[fmt.Sprintf("%sPTR", entry.Header().Name)] = &entry
}
}
}
}
for _, entry := range message.Ns {
if strings.HasSuffix(entry.Header().Name, ".local.") || domainInScopes(entry.Header().Name, localReverseScopes) {
if saveFullRequest {
k := indexOfRR(entry.Header(), &rrCache.Ns)
if k == -1 {
rrCache.Ns = append(rrCache.Ns, entry)
} else {
rrCache.Ns[k] = entry
}
}
switch entry.(type) {
case *dns.A:
scavengedRecords[fmt.Sprintf("%sA", entry.Header().Name)] = &entry
case *dns.AAAA:
scavengedRecords[fmt.Sprintf("%sAAAA", entry.Header().Name)] = &entry
case *dns.PTR:
if !strings.HasPrefix(entry.Header().Name, "_") {
scavengedRecords[fmt.Sprintf("%sPTR", entry.Header().Name)] = &entry
}
}
}
}
// TODO: scan Extra for A and AAAA records and save them seperately
for _, entry := range message.Extra {
if strings.HasSuffix(entry.Header().Name, ".local.") || domainInScopes(entry.Header().Name, localReverseScopes) {
if saveFullRequest {
k := indexOfRR(entry.Header(), &rrCache.Extra)
if k == -1 {
rrCache.Extra = append(rrCache.Extra, entry)
} else {
rrCache.Extra[k] = entry
}
}
switch entry.(type) {
case *dns.A:
scavengedRecords[fmt.Sprintf("%sA", entry.Header().Name)] = &entry
case *dns.AAAA:
scavengedRecords[fmt.Sprintf("%sAAAA", entry.Header().Name)] = &entry
case *dns.PTR:
if !strings.HasPrefix(entry.Header().Name, "_") {
scavengedRecords[fmt.Sprintf("%sPTR", entry.Header().Name)] = &entry
}
}
}
}
if saveFullRequest {
rrCache.Clean(60)
rrCache.CreateWithType(question.Name, dns.Type(question.Qtype))
// log.Tracef("intel: mdns saved full reply to %s%s", question.Name, dns.Type(question.Qtype).String())
}
for k, v := range scavengedRecords {
if saveFullRequest {
if k == fmt.Sprintf("%s%s", question.Name, dns.Type(question.Qtype).String()) {
continue
}
}
rrCache = &RRCache{
Answer: []dns.RR{*v},
}
rrCache.Clean(60)
rrCache.Create(k)
// log.Tracef("intel: mdns scavenged %s", k)
}
}
cleanSavedQuestions()
}
}
func listenForDNSPackets(conn *net.UDPConn, messages chan *dns.Msg) {
buf := make([]byte, 65536)
for {
// log.Tracef("debug: listening...")
n, err := conn.Read(buf)
// n, _, err := conn.ReadFrom(buf)
// n, _, err := conn.ReadFromUDP(buf)
if err != nil {
// log.Tracef("intel: failed to read packet: %s", err)
continue
}
// log.Tracef("debug: read something...")
message := new(dns.Msg)
if err = message.Unpack(buf[:n]); err != nil {
// log.Tracef("intel: failed to unpack message: %s", err)
continue
}
// log.Tracef("debug: parsed message...")
messages <- message
}
}
func queryMulticastDNS(resolver *Resolver, fqdn string, qtype dns.Type) (*RRCache, error) {
q := new(dns.Msg)
q.SetQuestion(fqdn, uint16(qtype))
// request unicast response
// q.Question[0].Qclass |= 1 << 15
q.RecursionDesired = false
saveQuestion(q)
questionsLock.Lock()
defer questionsLock.Unlock()
questions[q.MsgHdr.Id] = savedQuestion{
question: q.Question[0],
expires: time.Now().Add(10 * time.Second).Unix(),
}
buf, err := q.Pack()
if err != nil {
return nil, fmt.Errorf("failed to pack query: %s", err)
}
if unicast4Conn == nil && unicast6Conn == nil {
return nil, errors.New("unicast mdns connections not initialized")
}
if unicast4Conn != nil && uint16(qtype) != dns.TypeAAAA {
unicast4Conn.SetWriteDeadline(time.Now().Add(1 * time.Second))
_, err = unicast4Conn.WriteToUDP(buf, &net.UDPAddr{IP: net.IPv4(224, 0, 0, 251), Port: 5353})
if err != nil {
return nil, fmt.Errorf("failed to send query: %s", err)
}
}
if unicast6Conn != nil && uint16(qtype) != dns.TypeA {
unicast6Conn.SetWriteDeadline(time.Now().Add(1 * time.Second))
_, err = unicast6Conn.WriteToUDP(buf, &net.UDPAddr{IP: net.IP([]byte{0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfb}), Port: 5353})
if err != nil {
return nil, fmt.Errorf("failed to send query: %s", err)
}
}
time.Sleep(1 * time.Second)
rrCache, err := GetRRCache(fqdn, qtype)
if err == nil {
return rrCache, nil
}
return nil, nil
}
func saveQuestion(q *dns.Msg) {
questionsLock.Lock()
defer questionsLock.Unlock()
// log.Tracef("intel: saving mdns question id=%d, name=%s", q.MsgHdr.Id, q.Question[0].Name)
questions[q.MsgHdr.Id] = savedQuestion{
question: q.Question[0],
expires: time.Now().Add(10 * time.Second).Unix(),
}
}
func cleanSavedQuestions() {
questionsLock.Lock()
defer questionsLock.Unlock()
now := time.Now().Unix()
for k, v := range questions {
if v.expires < now {
delete(questions, k)
}
}
}

747
intel/resolve.go Normal file
View File

@@ -0,0 +1,747 @@
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
package intel
import (
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"math/rand"
"net"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/miekg/dns"
"github.com/tevino/abool"
"github.com/Safing/safing-core/configuration"
"github.com/Safing/safing-core/database"
"github.com/Safing/safing-core/log"
"github.com/Safing/safing-core/network/environment"
"github.com/Safing/safing-core/network/netutils"
)
// TODO: make resolver interface for http package
// special tlds:
// localhost. [RFC6761] - respond with 127.0.0.1 and ::1 to A and AAAA queries, else nxdomain
// local. [RFC6762] - resolve if search, else resolve with mdns
// 10.in-addr.arpa. [RFC6761]
// 16.172.in-addr.arpa. [RFC6761]
// 17.172.in-addr.arpa. [RFC6761]
// 18.172.in-addr.arpa. [RFC6761]
// 19.172.in-addr.arpa. [RFC6761]
// 20.172.in-addr.arpa. [RFC6761]
// 21.172.in-addr.arpa. [RFC6761]
// 22.172.in-addr.arpa. [RFC6761]
// 23.172.in-addr.arpa. [RFC6761]
// 24.172.in-addr.arpa. [RFC6761]
// 25.172.in-addr.arpa. [RFC6761]
// 26.172.in-addr.arpa. [RFC6761]
// 27.172.in-addr.arpa. [RFC6761]
// 28.172.in-addr.arpa. [RFC6761]
// 29.172.in-addr.arpa. [RFC6761]
// 30.172.in-addr.arpa. [RFC6761]
// 31.172.in-addr.arpa. [RFC6761]
// 168.192.in-addr.arpa. [RFC6761]
// 254.169.in-addr.arpa. [RFC6762]
// 8.e.f.ip6.arpa. [RFC6762]
// 9.e.f.ip6.arpa. [RFC6762]
// a.e.f.ip6.arpa. [RFC6762]
// b.e.f.ip6.arpa. [RFC6762]
// example. [RFC6761] - resolve if search, else return nxdomain
// example.com. [RFC6761] - resolve if search, else return nxdomain
// example.net. [RFC6761] - resolve if search, else return nxdomain
// example.org. [RFC6761] - resolve if search, else return nxdomain
// invalid. [RFC6761] - resolve if search, else return nxdomain
// test. [RFC6761] - resolve if search, else return nxdomain
// onion. [RFC7686] - resolve if search, else return nxdomain
// resolvers:
// local
// global
// mdns
// scopes:
// local-inaddr -> local, mdns
// local -> local scopes, mdns
// global -> local scopes, global
// special -> local scopes, local
type Resolver struct {
// static
Server string
ServerAddress string
IP *net.IP
Port uint16
Resolve func(resolver *Resolver, fqdn string, qtype dns.Type) (*RRCache, error)
Search *[]string
AllowedSecurityLevel int8
SkipFqdnBeforeInit string
HTTPClient *http.Client
Source string
// atomic
Initialized *abool.AtomicBool
InitLock sync.Mutex
LastFail *int64
Expires *int64
// must be locked
LockReason sync.Mutex
FailReason string
// TODO: add:
// Expiration (for server got from DHCP / ICMPv6)
// bootstrapping (first query is already sent, wait for it to either succeed or fail - think about http bootstrapping here!)
// expanded server info: type, server address, server port, options - so we do not have to parse this every time!
}
func (r *Resolver) String() string {
return r.Server
}
func (r *Resolver) Address() string {
return urlFormatAddress(r.IP, r.Port)
}
type Scope struct {
Domain string
Resolvers []*Resolver
}
var (
config = configuration.Get()
globalResolvers []*Resolver // all resolvers
localResolvers []*Resolver // all resolvers that are in site-local or link-local IP ranges
localScopes []Scope // list of scopes with a list of local resolvers that can resolve the scope
mDNSResolver *Resolver // holds a reference to the mDNS resolver
resolversLock sync.RWMutex
env = environment.NewInterface()
dupReqMap = make(map[string]*sync.Mutex)
dupReqLock sync.Mutex
)
func init() {
loadResolvers(false)
}
func indexOfResolver(server string, list []*Resolver) int {
for k, v := range list {
if v.Server == server {
return k
}
}
return -1
}
func indexOfScope(domain string, list *[]Scope) int {
for k, v := range *list {
if v.Domain == domain {
return k
}
}
return -1
}
func parseAddress(server string) (*net.IP, uint16, error) {
delimiter := strings.LastIndex(server, ":")
if delimiter < 0 {
return nil, 0, errors.New("port missing")
}
ip := net.ParseIP(strings.Trim(server[:delimiter], "[]"))
if ip == nil {
return nil, 0, errors.New("invalid IP address")
}
port, err := strconv.Atoi(server[delimiter+1:])
if err != nil || port < 1 || port > 65536 {
return nil, 0, errors.New("invalid port")
}
return &ip, uint16(port), nil
}
func urlFormatAddress(ip *net.IP, port uint16) string {
var address string
if ipv4 := ip.To4(); ipv4 != nil {
address = fmt.Sprintf("%s:%d", ipv4.String(), port)
} else {
address = fmt.Sprintf("[%s]:%d", ip.String(), port)
}
return address
}
func loadResolvers(resetResolvers bool) {
// TODO: what happens when a lot of processes want to reload at once? we do not need to run this multiple times in a short time frame.
resolversLock.Lock()
defer resolversLock.Unlock()
var newResolvers []*Resolver
configuredServersLoop:
for _, server := range config.DNSServers {
key := indexOfResolver(server, newResolvers)
if key >= 0 {
continue configuredServersLoop
}
key = indexOfResolver(server, globalResolvers)
if resetResolvers || key == -1 {
parts := strings.Split(server, "|")
if len(parts) < 2 {
log.Warningf("intel: invalid DNS server in config: %s (invalid format)", server)
continue configuredServersLoop
}
var lastFail int64
new := &Resolver{
Server: server,
ServerAddress: parts[1],
LastFail: &lastFail,
Source: "config",
Initialized: abool.NewBool(false),
}
ip, port, err := parseAddress(parts[1])
if err != nil {
new.IP = ip
new.Port = port
}
switch {
case strings.HasPrefix(server, "DNS|"):
new.Resolve = queryDNS
new.AllowedSecurityLevel = configuration.SecurityLevelFortress
case strings.HasPrefix(server, "DoH|"):
new.Resolve = queryDNSoverHTTPS
new.AllowedSecurityLevel = configuration.SecurityLevelFortress
new.SkipFqdnBeforeInit = dns.Fqdn(strings.Split(parts[1], ":")[0])
tls := &tls.Config{
// TODO: use custom random
// Rand: io.Reader,
}
tr := &http.Transport{
MaxIdleConnsPerHost: 100,
TLSClientConfig: tls,
// TODO: use custom resolver as of Go1.9
}
if len(parts) == 3 && strings.HasPrefix(parts[2], "df:") {
// activate domain fronting
tls.ServerName = parts[2][3:]
addDomainFronting(new.SkipFqdnBeforeInit, dns.Fqdn(tls.ServerName))
new.SkipFqdnBeforeInit = dns.Fqdn(tls.ServerName)
}
new.HTTPClient = &http.Client{Transport: tr}
default:
log.Warningf("intel: invalid DNS server in config: %s (not starting with a valid identifier)", server)
continue configuredServersLoop
}
newResolvers = append(newResolvers, new)
} else {
newResolvers = append(newResolvers, globalResolvers[key])
}
}
// add local resolvers
assignedNameservers := environment.Nameservers()
assignedServersLoop:
for _, nameserver := range assignedNameservers {
server := fmt.Sprintf("DNS|%s", urlFormatAddress(&nameserver.IP, 53))
key := indexOfResolver(server, newResolvers)
if key >= 0 {
continue assignedServersLoop
}
key = indexOfResolver(server, globalResolvers)
if resetResolvers || key == -1 {
var lastFail int64
new := &Resolver{
Server: server,
ServerAddress: urlFormatAddress(&nameserver.IP, 53),
IP: &nameserver.IP,
Port: 53,
LastFail: &lastFail,
Resolve: queryDNS,
AllowedSecurityLevel: configuration.SecurityLevelFortress,
Initialized: abool.NewBool(false),
Source: "dhcp",
}
if netutils.IPIsLocal(nameserver.IP) && len(nameserver.Search) > 0 {
// only allow searches for local resolvers
var newSearch []string
for _, value := range nameserver.Search {
newSearch = append(newSearch, fmt.Sprintf(".%s.", strings.Trim(value, ".")))
}
new.Search = &newSearch
}
newResolvers = append(newResolvers, new)
} else {
newResolvers = append(newResolvers, globalResolvers[key])
}
}
// save resolvers
globalResolvers = newResolvers
if len(globalResolvers) == 0 {
log.Criticalf("intel: no (valid) dns servers found in configuration and system")
}
// make list with local resolvers
localResolvers = make([]*Resolver, 0)
for _, resolver := range globalResolvers {
if resolver.IP != nil && netutils.IPIsLocal(*resolver.IP) {
localResolvers = append(localResolvers, resolver)
}
}
// add resolvers to every scope the cover
localScopes = make([]Scope, 0)
for _, resolver := range globalResolvers {
if resolver.Search != nil {
// add resolver to custom searches
for _, search := range *resolver.Search {
if search == "." {
continue
}
key := indexOfScope(search, &localScopes)
if key == -1 {
localScopes = append(localScopes, Scope{
Domain: search,
Resolvers: []*Resolver{resolver},
})
} else {
localScopes[key].Resolvers = append(localScopes[key].Resolvers, resolver)
}
}
}
}
// init mdns resolver
if mDNSResolver == nil {
cannotFail := int64(-1)
mDNSResolver = &Resolver{
Server: "mDNS",
Resolve: queryMulticastDNS,
AllowedSecurityLevel: config.DoNotUseMDNS.Level(),
Initialized: abool.NewBool(false),
Source: "static",
LastFail: &cannotFail,
}
}
// sort scopes by length
sort.Slice(localScopes,
func(i, j int) bool {
return len(localScopes[i].Domain) > len(localScopes[j].Domain)
},
)
log.Trace("intel: loaded global resolvers:")
for _, resolver := range globalResolvers {
log.Tracef("intel: %s", resolver.Server)
}
log.Trace("intel: loaded local resolvers:")
for _, resolver := range localResolvers {
log.Tracef("intel: %s", resolver.Server)
}
log.Trace("intel: loaded scopes:")
for _, scope := range localScopes {
var scopeServers []string
for _, resolver := range scope.Resolvers {
scopeServers = append(scopeServers, resolver.Server)
}
log.Tracef("intel: %s: %s", scope.Domain, strings.Join(scopeServers, ", "))
}
}
// Resolve resolves the given query for a domain and type and returns a RRCache object or nil, if the query failed.
func Resolve(fqdn string, qtype dns.Type, securityLevel int8) *RRCache {
fqdn = dns.Fqdn(fqdn)
// use this to time how long it takes resolve this domain
// timed := time.Now()
// defer log.Tracef("intel: took %s to get resolve %s%s", time.Now().Sub(timed).String(), fqdn, qtype.String())
// handle request for localhost
if fqdn == "localhost." {
var rr dns.RR
var err error
switch uint16(qtype) {
case dns.TypeA:
rr, err = dns.NewRR("localhost. 3600 IN A 127.0.0.1")
case dns.TypeAAAA:
rr, err = dns.NewRR("localhost. 3600 IN AAAA ::1")
default:
return nil
}
if err != nil {
return nil
}
return &RRCache{
Answer: []dns.RR{rr},
}
}
// check cache
rrCache, err := GetRRCache(fqdn, qtype)
if err != nil {
switch err {
case database.ErrNotFound:
default:
log.Warningf("intel: getting RRCache %s%s from database failed: %s", fqdn, qtype.String(), err)
}
return resolveAndCache(fqdn, qtype, securityLevel)
}
if rrCache.Expires <= time.Now().Unix() {
rrCache.requestingNew = true
go resolveAndCache(fqdn, qtype, securityLevel)
}
// randomize records to allow dumb clients (who only look at the first record) to reliably connect
for i := range rrCache.Answer {
j := rand.Intn(i + 1)
rrCache.Answer[i], rrCache.Answer[j] = rrCache.Answer[j], rrCache.Answer[i]
}
return rrCache
}
func resolveAndCache(fqdn string, qtype dns.Type, securityLevel int8) *RRCache {
// log.Tracef("intel: resolving %s%s", fqdn, qtype.String())
rrCache, ok := checkDomainFronting(fqdn, qtype, securityLevel)
if ok {
if rrCache == nil {
return nil
}
return rrCache
}
// dedup requests
dupKey := fmt.Sprintf("%s%s", fqdn, qtype.String())
dupReqLock.Lock()
mutex, requestActive := dupReqMap[dupKey]
if !requestActive {
mutex = new(sync.Mutex)
mutex.Lock()
dupReqMap[dupKey] = mutex
dupReqLock.Unlock()
} else {
dupReqLock.Unlock()
log.Tracef("intel: waiting for duplicate query for %s to complete", dupKey)
mutex.Lock()
// wait until duplicate request is finished, then fetch current RRCache and return
mutex.Unlock()
var err error
rrCache, err = GetRRCache(dupKey, qtype)
if err == nil {
return rrCache
}
// must have been nxdomain if we cannot get RRCache
return nil
}
defer func() {
dupReqLock.Lock()
delete(dupReqMap, fqdn)
dupReqLock.Unlock()
mutex.Unlock()
}()
// resolve
rrCache = intelligentResolve(fqdn, qtype, securityLevel)
if rrCache == nil {
return nil
}
// persist to database
rrCache.Clean(600)
rrCache.CreateWithType(fqdn, qtype)
return rrCache
}
func intelligentResolve(fqdn string, qtype dns.Type, securityLevel int8) *RRCache {
// TODO: handle being offline
// TODO: handle multiple network connections
if config.Changed() {
log.Info("intel: config changed, reloading resolvers")
loadResolvers(false)
} else if env.NetworkChanged() {
log.Info("intel: network changed, reloading resolvers")
loadResolvers(true)
}
config.RLock()
defer config.RUnlock()
resolversLock.RLock()
defer resolversLock.RUnlock()
lastFailBoundary := time.Now().Unix() - config.DNSServerRetryRate
preDottedFqdn := "." + fqdn
// resolve:
// reverse local -> local, mdns
// local -> local scopes, mdns
// special -> local scopes, local
// global -> local scopes, global
// local reverse scope
if domainInScopes(preDottedFqdn, localReverseScopes) {
// try local resolvers
for _, resolver := range localResolvers {
rrCache, ok := tryResolver(resolver, lastFailBoundary, fqdn, qtype, securityLevel)
if ok && rrCache != nil && !rrCache.IsNXDomain() {
return rrCache
}
}
// check config
if config.DoNotUseMDNS.IsSetWithLevel(securityLevel) {
return nil
}
// try mdns
rrCache, _ := tryResolver(mDNSResolver, lastFailBoundary, fqdn, qtype, securityLevel)
return rrCache
}
// local scopes
for _, scope := range localScopes {
if strings.HasSuffix(preDottedFqdn, scope.Domain) {
for _, resolver := range scope.Resolvers {
rrCache, ok := tryResolver(resolver, lastFailBoundary, fqdn, qtype, securityLevel)
if ok && rrCache != nil && !rrCache.IsNXDomain() {
return rrCache
}
}
}
}
switch {
case strings.HasSuffix(preDottedFqdn, ".local."):
// check config
if config.DoNotUseMDNS.IsSetWithLevel(securityLevel) {
return nil
}
// try mdns
rrCache, _ := tryResolver(mDNSResolver, lastFailBoundary, fqdn, qtype, securityLevel)
return rrCache
case domainInScopes(preDottedFqdn, specialScopes):
// check config
if config.DoNotForwardSpecialDomains.IsSetWithLevel(securityLevel) {
return nil
}
// try local resolvers
for _, resolver := range localResolvers {
rrCache, ok := tryResolver(resolver, lastFailBoundary, fqdn, qtype, securityLevel)
if ok {
return rrCache
}
}
default:
// try global resolvers
for _, resolver := range globalResolvers {
rrCache, ok := tryResolver(resolver, lastFailBoundary, fqdn, qtype, securityLevel)
if ok {
return rrCache
}
}
}
log.Criticalf("intel: failed to resolve %s%s: all resolvers failed (or were skipped to fulfill the security level)", fqdn, qtype.String())
return nil
// TODO: check if there would be resolvers available in lower security modes and alert user
}
func tryResolver(resolver *Resolver, lastFailBoundary int64, fqdn string, qtype dns.Type, securityLevel int8) (*RRCache, bool) {
// skip if not allowed in current security level
if resolver.AllowedSecurityLevel < config.SecurityLevel() || resolver.AllowedSecurityLevel < securityLevel {
log.Tracef("intel: skipping resolver %s, because it isn't allowed to operate on the current security level: %d|%d", resolver, config.SecurityLevel(), securityLevel)
return nil, false
}
// skip if not security level denies assigned dns servers
if config.DoNotUseAssignedDNS.IsSetWithLevel(securityLevel) && resolver.Source == "dhcp" {
log.Tracef("intel: skipping resolver %s, because assigned nameservers are not allowed on the current security level: %d|%d (%d)", resolver, config.SecurityLevel(), securityLevel, int8(config.DoNotUseAssignedDNS))
return nil, false
}
// check if failed recently
if atomic.LoadInt64(resolver.LastFail) > lastFailBoundary {
return nil, false
}
// TODO: put SkipFqdnBeforeInit back into !resolver.Initialized.IsSet() as soon as Go1.9 arrives and we can use a custom resolver
// skip resolver if initializing and fqdn is set to skip
if fqdn == resolver.SkipFqdnBeforeInit {
return nil, false
}
// check if resolver is already initialized
if !resolver.Initialized.IsSet() {
// first should init, others wait
resolver.InitLock.Lock()
if resolver.Initialized.IsSet() {
// unlock immediately if resolver was initialized
resolver.InitLock.Unlock()
} else {
// initialize and unlock when finished
defer resolver.InitLock.Unlock()
}
// check if previous init failed
if atomic.LoadInt64(resolver.LastFail) > lastFailBoundary {
return nil, false
}
}
// resolve
log.Tracef("intel: trying to resolve %s%s with %s", fqdn, qtype.String(), resolver.Server)
rrCache, err := resolver.Resolve(resolver, fqdn, qtype)
if err != nil {
// check if failing is disabled
if atomic.LoadInt64(resolver.LastFail) == -1 {
log.Tracef("intel: non-failing resolver %s failed (%s), moving to next", resolver, err)
return nil, false
}
log.Warningf("intel: resolver %s failed (%s), moving to next", resolver, err)
resolver.LockReason.Lock()
resolver.FailReason = err.Error()
resolver.LockReason.Unlock()
atomic.StoreInt64(resolver.LastFail, time.Now().Unix())
resolver.Initialized.UnSet()
return nil, false
}
resolver.Initialized.SetToIf(false, true)
return rrCache, true
}
func queryDNS(resolver *Resolver, fqdn string, qtype dns.Type) (*RRCache, error) {
q := new(dns.Msg)
q.SetQuestion(fqdn, uint16(qtype))
var reply *dns.Msg
var err error
for i := 0; i < 5; i++ {
client := new(dns.Client)
reply, _, err = client.Exchange(q, resolver.ServerAddress)
if err != nil {
// TODO: handle special cases
// 1. connect: network is unreachable
// 2. timeout
if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
log.Tracef("intel: retrying to resolve %s%s with %s, error was: %s", fqdn, qtype.String(), resolver.Server, err)
continue
}
break
}
}
if err != nil {
log.Warningf("resolving %s%s failed: %s", fqdn, qtype.String(), err)
return nil, fmt.Errorf("resolving %s%s failed: %s", fqdn, qtype.String(), err)
}
new := &RRCache{
Answer: reply.Answer,
Ns: reply.Ns,
Extra: reply.Extra,
}
// TODO: check if reply.Answer is valid
return new, nil
}
type DnsOverHttpsReply struct {
Status uint32
Truncated bool `json:"TC"`
Answer []DohRR
Additional []DohRR
}
type DohRR struct {
Name string `json:"name"`
Qtype uint16 `json:"type"`
TTL uint32 `json:"TTL"`
Data string `json:"data"`
}
func queryDNSoverHTTPS(resolver *Resolver, fqdn string, qtype dns.Type) (*RRCache, error) {
// API documentation: https://developers.google.com/speed/public-dns/docs/dns-over-https
payload := url.Values{}
payload.Add("name", fqdn)
payload.Add("type", strconv.Itoa(int(qtype)))
payload.Add("edns_client_subnet", "0.0.0.0/0")
// TODO: add random - only use upper- and lower-case letters, digits, hyphen, period, underscore and tilde
// payload.Add("random_padding", "")
resp, err := resolver.HTTPClient.Get(fmt.Sprintf("https://%s/resolve?%s", resolver.ServerAddress, payload.Encode()))
if err != nil {
return nil, fmt.Errorf("resolving %s%s failed: http error: %s", fqdn, qtype.String(), err)
// TODO: handle special cases
// 1. connect: network is unreachable
// intel: resolver DoH|dns.google.com:443|df:www.google.com failed (resolving discovery-v4-4.syncthing.net.A failed: http error: Get https://dns.google.com:443/resolve?edns_client_subnet=0.0.0.0%2F0&name=discovery-v4-4.syncthing.net.&type=1: dial tcp [2a00:1450:4001:819::2004]:443: connect: network is unreachable), moving to next
// 2. timeout
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("resolving %s%s failed: request was unsuccessful, got code %d", fqdn, qtype.String(), resp.StatusCode)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("resolving %s%s failed: error reading response body: %s", fqdn, qtype.String(), err)
}
var reply DnsOverHttpsReply
err = json.Unmarshal(body, &reply)
if err != nil {
return nil, fmt.Errorf("resolving %s%s failed: error parsing response body: %s", fqdn, qtype.String(), err)
}
if reply.Status != 0 {
// this happens if there is a server error (e.g. DNSSEC fail), ignore for now
// TODO: do something more intelligent
}
new := new(RRCache)
// TODO: handle TXT records
for _, entry := range reply.Answer {
rr, err := dns.NewRR(fmt.Sprintf("%s %d IN %s %s", entry.Name, entry.TTL, dns.Type(entry.Qtype).String(), entry.Data))
if err != nil {
log.Warningf("intel: resolving %s%s failed: failed to parse record to DNS: %s %d IN %s %s", fqdn, qtype.String(), entry.Name, entry.TTL, dns.Type(entry.Qtype).String(), entry.Data)
continue
}
new.Answer = append(new.Answer, rr)
}
for _, entry := range reply.Additional {
rr, err := dns.NewRR(fmt.Sprintf("%s %d IN %s %s", entry.Name, entry.TTL, dns.Type(entry.Qtype).String(), entry.Data))
if err != nil {
log.Warningf("intel: resolving %s%s failed: failed to parse record to DNS: %s %d IN %s %s", fqdn, qtype.String(), entry.Name, entry.TTL, dns.Type(entry.Qtype).String(), entry.Data)
continue
}
new.Extra = append(new.Extra, rr)
}
return new, nil
}
// TODO: implement T-DNS: DNS over TCP/TLS
// server list: https://dnsprivacy.org/wiki/display/DP/DNS+Privacy+Test+Servers

15
intel/resolve_test.go Normal file
View File

@@ -0,0 +1,15 @@
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
package intel
import (
"testing"
"time"
"github.com/miekg/dns"
)
func TestResolve(t *testing.T) {
Resolve("google.com.", dns.Type(dns.TypeA), 0)
time.Sleep(200 * time.Millisecond)
}

52
intel/special.go Normal file
View File

@@ -0,0 +1,52 @@
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
package intel
import "strings"
var (
localReverseScopes = &[]string{
".10.in-addr.arpa.",
".16.172.in-addr.arpa.",
".17.172.in-addr.arpa.",
".18.172.in-addr.arpa.",
".19.172.in-addr.arpa.",
".20.172.in-addr.arpa.",
".21.172.in-addr.arpa.",
".22.172.in-addr.arpa.",
".23.172.in-addr.arpa.",
".24.172.in-addr.arpa.",
".25.172.in-addr.arpa.",
".26.172.in-addr.arpa.",
".27.172.in-addr.arpa.",
".28.172.in-addr.arpa.",
".29.172.in-addr.arpa.",
".30.172.in-addr.arpa.",
".31.172.in-addr.arpa.",
".168.192.in-addr.arpa.",
".254.169.in-addr.arpa.",
".8.e.f.ip6.arpa.",
".9.e.f.ip6.arpa.",
".a.e.f.ip6.arpa.",
".b.e.f.ip6.arpa.",
}
specialScopes = &[]string{
".example.",
".example.com.",
".example.net.",
".example.org.",
".invalid.",
".test.",
".onion.",
}
)
func domainInScopes(fqdn string, list *[]string) bool {
for _, scope := range *list {
if strings.HasSuffix(fqdn, scope) {
return true
}
}
return false
}