├── nettrust.service ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ └── ci.yml ├── firewall ├── errors.go ├── nftables │ ├── errors.go │ ├── nft4.go │ ├── nft.go │ ├── nft4net.go │ ├── nft4set.go │ └── chain4.go └── firewall.go ├── .gitignore ├── core ├── trace │ └── errors.go ├── signals.go ├── errors.go ├── checks.go ├── flags.go └── env.go ├── config.json ├── go.mod ├── LICENSE ├── dns ├── errors.go ├── dns.go ├── proxy.go ├── cache │ └── cache.go └── listeners.go ├── authorizer ├── conntrack.go ├── errors.go ├── cache │ └── cache.go ├── authorizer.go ├── activity.go └── handler.go ├── ToDo.md ├── cmd └── nettrust.go ├── go.sum └── README.md /nettrust.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=NetTrust Firewall Authorizer 3 | Wants=network.target 4 | After=network.target 5 | 6 | [Service] 7 | ExecStart=/usr/local/bin/nettrust -config /etc/nettrust/config.json 8 | WorkingDirectory=/etc/nettrust 9 | Restart=always 10 | 11 | [Install] 12 | WantedBy=multi-user.target 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Template for bug reporting 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behaviour: 15 | 16 | **Expected behaviour** 17 | 18 | **Additional context** 19 | Add any other context about the problem here. 20 | -------------------------------------------------------------------------------- /firewall/errors.go: -------------------------------------------------------------------------------- 1 | package firewall 2 | 3 | var ( 4 | errFWDHook string = "not supported firewall hook [%s]" 5 | errNotSupportedFWDBackend string = "[%s] is not yet supported" 6 | errUnknownFWDBackend string = "not supported firewall backend [%s]" 7 | errEmptyName string = "%s name not allowed to be empty" 8 | infoFWDCreate string = "creating [%s] rules" 9 | infoFWDInput string = "creating input rules" 10 | ) 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | nettrust 18 | tests 19 | Makefile 20 | packages 21 | dns.pem 22 | dns.key 23 | dev 24 | load-test.sh 25 | heap.out 26 | *.pdf 27 | memleak.sh 28 | 29 | -------------------------------------------------------------------------------- /firewall/nftables/errors.go: -------------------------------------------------------------------------------- 1 | package nftables 2 | 3 | var ( 4 | errNoSuchCahin string = "could not find chain [%s]" 5 | errNoSuchTable string = "could not find table [%s]" 6 | errNotValidIPv4Addr string = "[%s] does not appear to be a valid ipv4 ipaddr" 7 | errNotSuchIPv4NetRule string = "could not find network rule with cidr [%s]" 8 | errNotSuchIPv4AddrRule string = "could not find rule with ip [%s]" 9 | errNoSuchIPv4SetRule string = "could not find set rule with name [%s]" 10 | ) 11 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | 11 | Lint: 12 | name: Lint 13 | runs-on: ubuntu-latest 14 | steps: 15 | 16 | - name: Set up Go 1.x 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: ^1.13 20 | 21 | - name: Check out code into the Go module directory 22 | uses: actions/checkout@v2 23 | 24 | - name: Get dependencies 25 | run: | 26 | go get -v -t -d ./... 27 | 28 | - name: Get Lint 29 | run: go get -u golang.org/x/lint/golint 30 | 31 | - name: Lint 32 | run: golint -set_exit_status ./... 33 | -------------------------------------------------------------------------------- /core/trace/errors.go: -------------------------------------------------------------------------------- 1 | package trace 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | "strconv" 7 | 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | // WrapErr for creating errors and wrapping them along 12 | // with callers info 13 | func WrapErr(e interface{}, p ...interface{}) error { 14 | if e == nil { 15 | return nil 16 | } 17 | 18 | var err error 19 | 20 | switch e := e.(type) { 21 | case string: 22 | err = fmt.Errorf(e, p...) 23 | case error: 24 | err = e 25 | } 26 | 27 | pc, _, no, ok := runtime.Caller(1) 28 | details := runtime.FuncForPC(pc) 29 | if ok && details != nil { 30 | return errors.Wrap(err, fmt.Sprintf("%s#%s\n", details.Name(), strconv.Itoa(no))) 31 | } 32 | 33 | return err 34 | } 35 | -------------------------------------------------------------------------------- /core/signals.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "os" 5 | "os/signal" 6 | "syscall" 7 | ) 8 | 9 | // OSSignalHandler for storing a signal 10 | type OSSignalHandler struct { 11 | Signal chan os.Signal 12 | } 13 | 14 | // NewOSSignal for creating a new signal 15 | func NewOSSignal() OSSignalHandler { 16 | osSig := OSSignalHandler{} 17 | 18 | osSig.Signal = make(chan os.Signal, 2) 19 | signal.Notify( 20 | osSig.Signal, 21 | syscall.SIGINT, 22 | syscall.SIGTERM, 23 | os.Interrupt, 24 | ) 25 | 26 | return osSig 27 | } 28 | 29 | // Wait for waiting for an OS signal 30 | func (s *OSSignalHandler) Wait() { 31 | <-s.Signal 32 | } 33 | 34 | // Close channel 35 | func (s *OSSignalHandler) Close() { 36 | close(s.Signal) 37 | } 38 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "whitelist": { 3 | "networks": [], 4 | "hosts": [] 5 | }, 6 | "blacklist": { 7 | "networks": [], 8 | "hosts": [], 9 | "domains": [] 10 | }, 11 | "fwdAddr": "", 12 | "fwdProto": "udp", 13 | "fwdCaCert": "", 14 | "fwdTLS": false, 15 | "fwdUDPBufferSize": 4096, 16 | 17 | "listenAddr": "127.0.0.1:53", 18 | "listenTLS": false, 19 | "listenCert": "", 20 | "listenCertKey": "", 21 | "firewallBackend": "nftables", 22 | "firewallType": "OUTPUT", 23 | "firewallDropInput": false, 24 | 25 | "dnsTTLCache": -1, 26 | 27 | "whitelistLoEnabled": true, 28 | "whitelistPrivateEnabled": true, 29 | "ttl": -1, 30 | "ttlInterval": 30, 31 | "doNotFlushTable": false, 32 | "doNotFlushAuthorizedHosts": false 33 | } 34 | -------------------------------------------------------------------------------- /core/errors.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | var ( 4 | errSameAddr string = "listen address can not be the same as forward address" 5 | errListenTLSNoFile string = "listen-tls is enabled but no %s was provided" 6 | errInvalidSocketAddress string = "address [%s] is not valid. Expected ip:port" 7 | errInvalidPort string = "invalid port [%d] number" 8 | errNotValidIPv4Addr string = "not a valid ipv4 address [%s]" 9 | errNotValidIPv4Network string = "not a valid ipv4 network [%s]" 10 | 11 | // WarnOnExitFlushAuthorized will be printed when authorized hosts are preserved on NetTrust exit 12 | WarnOnExitFlushAuthorized string = "on exit NetTrust will not flush the authorized hosts list" 13 | 14 | // WarnOnExitFlush will be printed when on exit flush table is enabled 15 | WarnOnExitFlush string = "on exit flush table is enabled. Please set this to false if you wish to deny traffic to all if NetTrust is not running" 16 | ) 17 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ulfox/nettrust 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/google/nftables v0.0.0-20220210072902-edf9fe8cd04f 7 | github.com/miekg/dns v1.1.46 8 | github.com/pkg/errors v0.9.1 9 | github.com/sirupsen/logrus v1.8.1 10 | github.com/ti-mo/conntrack v0.4.0 11 | ) 12 | 13 | require ( 14 | github.com/BurntSushi/toml v0.4.1 // indirect 15 | github.com/google/go-cmp v0.5.6 // indirect 16 | github.com/josharian/native v0.0.0-20200817173448-b6b71def0850 // indirect 17 | github.com/mdlayher/netlink v1.4.2 // indirect 18 | github.com/mdlayher/socket v0.0.0-20211102153432-57e3fa563ecb // indirect 19 | github.com/ti-mo/netfilter v0.3.1 // indirect 20 | golang.org/x/mod v0.5.1 // indirect 21 | golang.org/x/net v0.0.0-20211209124913-491a49abca63 // indirect 22 | golang.org/x/sys v0.0.0-20211205182925-97ca703d548d // indirect 23 | golang.org/x/tools v0.1.8 // indirect 24 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 25 | honnef.co/go/tools v0.2.2 // indirect 26 | ) 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Christos Kotsis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /dns/errors.go: -------------------------------------------------------------------------------- 1 | package dns 2 | 3 | var ( 4 | errFWDNSAddr string = "forward dns host: addr can not be empty" 5 | errFWDNSAddrInvalid string = "forward dns address is not valid [%s:%s]" 6 | errFWDNSProto string = "forward tcp proto can be either tcp or udp" 7 | errFWDTLS string = "forward tls requires proto to be tcp" 8 | errQuery string = "invalid query, no questions" 9 | errNotAFile string = "[%s] is a directory" 10 | errManyQuestions string = "[Invalid] query has more than 1 question [%s]" 11 | errCacheFetch string = "[Cache] something went wrong, could not fetch dns object from cache for question: %s" 12 | errCacheRegister string = "[Cache] could not register dns object with question %s to cache" 13 | errCacheCoulndNotRenew string = "[Cache] could not renew object for question %s" 14 | errNil string = "cache has not been initialized, starting ttl cache checker is forbidden" 15 | warnFWDTLSPort string = "forward tls is enabled but port is set to 53" 16 | infoCacheObjExpired string = "[Cache] dns cache object with question %s has expired, asking upstream" 17 | infoCacheObjFound string = "[Cache] found dns object in cache for question %s" 18 | infoCacheObjFoundNil string = "[Cache] found dns object in nil cache for question %s" 19 | infoDomainBlacklist string = "[Blacklisted] Question %s" 20 | ) 21 | -------------------------------------------------------------------------------- /authorizer/conntrack.go: -------------------------------------------------------------------------------- 1 | package authorizer 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // conntrackDump, not blocking for now. For now, we expect activeHosts to 9 | // be read only by ttl checker and written only by this method 10 | func (f *Authorizer) conntrackDump() (map[string]struct{}, error) { 11 | if f.conntrack == nil { 12 | return nil, fmt.Errorf(errNil) 13 | } 14 | 15 | df, err := f.conntrack.Dump() 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | activeHosts := map[string]struct{}{} 21 | 22 | // This call is a bit slow on systems with a huge number of active connections, however it is a good 23 | // way to ensure that we get in the map hosts that are part of source or destination 24 | // 25 | // If this is reported to be too slow, then we should look for a better solution 26 | // The good thing is that this call is not blocking. The ttl checker may hang while waiting for 27 | // the activeHosts list, however during the wait, RequestHandler is free to call cache 28 | for _, j := range df { 29 | activeHosts[strings.Split(j.TupleOrig.IP.DestinationAddress.String(), ":")[0]] = struct{}{} 30 | activeHosts[strings.Split(j.TupleOrig.IP.SourceAddress.String(), ":")[0]] = struct{}{} 31 | activeHosts[strings.Split(j.TupleReply.IP.DestinationAddress.String(), ":")[0]] = struct{}{} 32 | activeHosts[strings.Split(j.TupleReply.IP.SourceAddress.String(), ":")[0]] = struct{}{} 33 | } 34 | 35 | return activeHosts, nil 36 | } 37 | -------------------------------------------------------------------------------- /authorizer/errors.go: -------------------------------------------------------------------------------- 1 | package authorizer 2 | 3 | var ( 4 | errNil string = "authorizer has not been initialized, starting ttl cache checker is forbidden" 5 | errTTL string = "ttl ticker can not be 0 or negative" 6 | errSetName string = "authorized set can not be empty" 7 | errInvalidReply string = "[Invalid] query has Qtype %s but we could not read answer for question: %s" 8 | errRcode string = "[QuerryError] query [%s] returned rcode different than success or nxdomain. Rcode [%d]" 9 | warnTTL string = "ttl ticker is set to be %d sec. Please note that cache checks are blocking, frequent calls means frequent blocks" 10 | warnPTRIPv6 string = "[PTR IPv6] Question %s resolved to %s but was not authorized. NetTrust does not support IPv6 yet" 11 | warnIPv6Support string = "[IPv6] Question: %s Host: %s NetTrust does not support IPv6 yet" 12 | warnNotSupportedQuery string = "[Not Supported] Question type [%d] for question %s" 13 | infoPTRIPv4 string = "[PTR IPv4] Question %s with host %s resolved to %s" 14 | infoAuthBlacklist string = "[Blacklisted] Question %s Host: %s" 15 | infoAuthBlock string = "[No Answer] Question %s" 16 | infoAuthIPv6Block string = "[No Answer] IPv6 Question %s" 17 | infoAuthExists string = "[Already Authorized] Question %s Host: %s" 18 | infoAuth string = "[Authorized] Question %s Hosts: [%s]" 19 | infoNXDomain string = "[Name Error] Question %s returned NX Domain" 20 | ) 21 | -------------------------------------------------------------------------------- /core/checks.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | func emptyStringE(s string) error { 10 | if s == "" { 11 | return fmt.Errorf("is empty") 12 | } 13 | 14 | return nil 15 | } 16 | 17 | // CheckIPV4SocketAddress checks if input strings can be split into ip/port pairs 18 | func CheckIPV4SocketAddress(address string) error { 19 | strSlice := strings.Split(address, ":") 20 | if len(strSlice) != 2 { 21 | return fmt.Errorf(errInvalidSocketAddress, address) 22 | } 23 | 24 | if es := emptyStringE(strSlice[0]); es != nil { 25 | return fmt.Errorf("IP [%s] %s", address, es) 26 | } 27 | if es := emptyStringE(strSlice[1]); es != nil { 28 | return fmt.Errorf("port [%s] %s", address, es) 29 | } 30 | 31 | port, err := strconv.Atoi(strSlice[1]) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | if port < 1 || port > 65535 { 37 | return fmt.Errorf(errInvalidPort, port) 38 | } 39 | 40 | if err := CheckIPV4Addresses(strSlice[0]); err != nil { 41 | return err 42 | } 43 | 44 | return nil 45 | } 46 | 47 | // CheckIPV4Addresses simple method for checking if an address is a correct 48 | // ipv4 address 49 | func CheckIPV4Addresses(addr string) error { 50 | ipSlice := strings.Split(addr, ".") 51 | 52 | for _, oct := range ipSlice { 53 | octInt, err := strconv.Atoi(oct) 54 | if err != nil { 55 | return err 56 | } 57 | if octInt < 0 || octInt > 255 { 58 | return fmt.Errorf(errNotValidIPv4Addr, addr) 59 | } 60 | } 61 | 62 | return nil 63 | } 64 | 65 | // CheckIPV4Network simple method for checking if a network is a correct 66 | // ipv4 network 67 | func CheckIPV4Network(addr string) error { 68 | netSlice := strings.Split(addr, "/") 69 | err := CheckIPV4Addresses(netSlice[0]) 70 | if err != nil { 71 | return err 72 | } 73 | 74 | cidr, err := strconv.Atoi(netSlice[1]) 75 | if err != nil { 76 | return err 77 | } 78 | 79 | if cidr < 0 || cidr > 32 { 80 | return fmt.Errorf(errNotValidIPv4Network, addr) 81 | } 82 | 83 | return nil 84 | } 85 | -------------------------------------------------------------------------------- /authorizer/cache/cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | // Authorized for storing Authorized DNS Hosts 9 | type Authorized struct { 10 | sync.Mutex 11 | TTL int 12 | Hosts map[string]time.Time 13 | } 14 | 15 | // NewCache creates a new empty cache 16 | func NewCache(ttl int) *Authorized { 17 | return &Authorized{ 18 | TTL: ttl, 19 | Hosts: make(map[string]time.Time), 20 | } 21 | } 22 | 23 | // NewAuthMap replaces current cache.Hosts map with a new map 24 | // by copying all the elements. We this to free up memory, since 25 | // the allocated memory by cache.Hosts is that that the map had at 26 | // its peak 27 | func (c *Authorized) NewAuthMap() { 28 | c.Lock() 29 | defer c.Unlock() 30 | 31 | newMap := make(map[string]time.Time) 32 | 33 | for k, v := range c.Hosts { 34 | newMap[k] = v 35 | } 36 | 37 | c.Hosts = nil 38 | c.Hosts = newMap 39 | } 40 | 41 | // Exists (blocking) returns true if a host is in cache 42 | func (c *Authorized) Exists(h string) bool { 43 | c.Lock() 44 | defer c.Unlock() 45 | _, ok := c.Hosts[h] 46 | 47 | return ok 48 | } 49 | 50 | // Register (blocking) for adding a new host to cache 51 | func (c *Authorized) Register(h string) bool { 52 | if c.Exists(h) { 53 | return false 54 | } 55 | 56 | c.Lock() 57 | defer c.Unlock() 58 | 59 | c.Hosts[h] = time.Now() 60 | 61 | return true 62 | } 63 | 64 | // Renew (blocking) for updating a hosts registration time in cache 65 | func (c *Authorized) Renew(h string) { 66 | c.Lock() 67 | defer c.Unlock() 68 | 69 | c.Hosts[h] = time.Now() 70 | } 71 | 72 | // Expired (blocking) for returning all expired hosts. Returns empty slice if c.TTL is < 0 73 | func (c *Authorized) Expired() []string { 74 | if c.TTL < 0 { 75 | return []string{} 76 | } 77 | 78 | c.Lock() 79 | defer c.Unlock() 80 | 81 | hosts := []string{} 82 | for h, t := range c.Hosts { 83 | if time.Since(t) > time.Second*time.Duration(c.TTL) { 84 | hosts = append(hosts, h) 85 | } 86 | } 87 | 88 | return hosts 89 | } 90 | 91 | // Delete (blocking) for deleting a host from cache 92 | func (c *Authorized) Delete(h string) { 93 | c.Lock() 94 | defer c.Unlock() 95 | delete(c.Hosts, h) 96 | } 97 | -------------------------------------------------------------------------------- /ToDo.md: -------------------------------------------------------------------------------- 1 | # ToDo List 2 | 3 | ## Fix 4 | 5 | - Conntrack activeHosts throws netfilter query error on MIPS64 SF 6 | 7 | ## Improvements 8 | 9 | - Check chains priority and ensure NetTrust Chain has high priority in the related table 10 | 11 | ## Features 12 | 13 | - (Depends on namespace filtering and Nettrust follower agents & K8 network policies features) Kubernetes operator. Allow nettrust to be deployed and managed by a K8 operator. Nettrust can use coredns or other dns authorizers to filter the outbound traffic within the nodes 14 | - Nettrust follower agent. In this feature Nettrust can run as a follower. While in follower mode, it will query a master agent or operator in order: fetch the configuration, use a shared authorized map. 15 | - Nettrust K8 Network Policies. Allow Nettrust to filter traffic using K8 Network policies instead of nftables. In this mode Nettrust will not need elevated privileges 16 | - When filtering forward chain, allow Nettrust to whitelist hosts by source network. This feature can allow the gateway where nettrust is running to allow connections to 0.0.0.0/0 that are sourced from a subnet/s and deny connections that have not been authorized for all other subnets 17 | - Cloud provider plugin 18 | - Add option for TLS Client authendication 19 | - Add eBPF filtering to allow NetTrust block packets before they enter the Kenrel network stack 20 | - Add network namespace filtering option. This can be achieved by making the firewall backend an array and loop over each time a command is executed to handle multipe namespaces 21 | - DNS listen strikes on many invalid/block requests 22 | - Handle IPv6 also 23 | - Add support for reverse queries, essentially whitelisting IPs if the DNS Authorizer returns a domain back to NetTrust 24 | - Add metrics capabilities to monitor NetTrust 25 | - Add network statistics (e.g. how many times a host was queried) to allow alerts/notifications on certain events 26 | - Add DNSSec 27 | - Add IPTables Support (iptables-legacy, iptables-nft) 28 | - Add option to use a KV store for keeping host tracking information 29 | - Use conntrack to check and react on connections that open and are not part of NetTrust whitelisted hosts 30 | - Conntrack Hosts & ttl metrics 31 | - Add option to handle A/AAA zones instead of forwarding all requests 32 | - Add option to watch for /etc/resolv.conf changes and revert back to NetTrust listening address 33 | - Add DNS Forward loadbalance option (to allow usage of more than 1 DNS server) 34 | - Add debug logs 35 | 36 | -------------------------------------------------------------------------------- /firewall/nftables/nft4.go: -------------------------------------------------------------------------------- 1 | package nftables 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "strings" 7 | 8 | "github.com/google/nftables" 9 | "github.com/google/nftables/expr" 10 | ) 11 | 12 | func (f *FirewallBackend) getIPv4Rule(ip string) (*nftables.Rule, error) { 13 | f.Lock() 14 | rules, err := f.nft.GetRule(f.table, f.chain) 15 | f.Unlock() 16 | 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | for _, rule := range rules { 22 | for _, e := range rule.Exprs { 23 | cmp, ok := e.(*expr.Cmp) 24 | if !ok { 25 | continue 26 | } 27 | if net.IP(cmp.Data).String() == ip { 28 | return rule, nil 29 | } 30 | } 31 | } 32 | 33 | return nil, fmt.Errorf(errNotSuchIPv4AddrRule, ip) 34 | } 35 | 36 | // AddIPv4Rule for adding IPv4 rules in the chain, should never be used after initial chain setup 37 | func (f *FirewallBackend) AddIPv4Rule(ip string) error { 38 | netIP := net.ParseIP(ip).To4() 39 | if netIP == nil { 40 | return fmt.Errorf(errNotValidIPv4Addr, ip) 41 | } 42 | 43 | _, err := f.getIPv4Rule(ip) 44 | if err != nil { 45 | if !strings.HasPrefix(err.Error(), "could not find rule with ip") { 46 | return err 47 | } 48 | } else { 49 | return nil 50 | } 51 | 52 | f.Lock() 53 | defer f.Unlock() 54 | 55 | f.nft.AddRule(&nftables.Rule{ 56 | Table: f.table, 57 | Chain: f.chain, 58 | Exprs: []expr.Any{ 59 | &expr.Payload{ 60 | OperationType: expr.PayloadLoad, 61 | DestRegister: 1, 62 | Base: expr.PayloadBaseNetworkHeader, 63 | Offset: 16, 64 | Len: 4, 65 | }, 66 | &expr.Cmp{ 67 | Op: expr.CmpOpEq, 68 | Register: 1, 69 | Data: netIP, 70 | }, 71 | &expr.Counter{}, 72 | &expr.Verdict{Kind: expr.VerdictAccept}, 73 | }, 74 | }) 75 | 76 | return f.nft.Flush() 77 | } 78 | 79 | // DeleteIPv4Rule for deleting an IPv4 rule from the chain 80 | func (f *FirewallBackend) DeleteIPv4Rule(ip string) error { 81 | netIP := net.ParseIP(ip).To4() 82 | if netIP == nil { 83 | return fmt.Errorf(errNotValidIPv4Addr, ip) 84 | } 85 | 86 | r, err := f.getIPv4Rule(ip) 87 | if err != nil { 88 | if !strings.HasPrefix(err.Error(), "could not find rule with ip") { 89 | return err 90 | } 91 | return nil 92 | } 93 | 94 | r.Chain = f.chain 95 | r.Table = f.table 96 | 97 | f.Lock() 98 | defer f.Unlock() 99 | 100 | err = f.nft.DelRule(r) 101 | if err != nil { 102 | return err 103 | } 104 | return f.nft.Flush() 105 | } 106 | -------------------------------------------------------------------------------- /authorizer/authorizer.go: -------------------------------------------------------------------------------- 1 | package authorizer 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/sirupsen/logrus" 8 | "github.com/ti-mo/conntrack" 9 | "github.com/ulfox/nettrust/authorizer/cache" 10 | "github.com/ulfox/nettrust/firewall" 11 | ) 12 | 13 | // Authorizer for managing firewall rules 14 | type Authorizer struct { 15 | logger *logrus.Logger 16 | fw *firewall.Firewall 17 | fwl *logrus.Entry 18 | cache *cache.Authorized 19 | conntrack *conntrack.Conn 20 | blacklistHosts, blacklistNetworks []string 21 | ttl, ttlCheckTicker int 22 | authorizedSet string 23 | doNotFlushAuthorizedHosts bool 24 | } 25 | 26 | // NewAuthorizer for creating a new Authorizer 27 | func NewAuthorizer( 28 | ttl, 29 | ttlCheckTicker int, 30 | authorizedSet string, 31 | blacklistHosts, blacklistNetworks []string, 32 | doNotFlushAuthorizedHosts bool, 33 | fw *firewall.Firewall, 34 | logger *logrus.Logger) (*Authorizer, *ServiceContext, error) { 35 | 36 | authorizer := &Authorizer{ 37 | logger: logger, 38 | fwl: logger.WithFields(logrus.Fields{ 39 | "Component": "Firewall", 40 | "Stage": "Authorizer", 41 | }), 42 | blacklistHosts: blacklistHosts, 43 | blacklistNetworks: blacklistNetworks, 44 | ttl: ttl, 45 | ttlCheckTicker: ttlCheckTicker, 46 | authorizedSet: authorizedSet, 47 | fw: fw, 48 | cache: cache.NewCache(ttl), 49 | doNotFlushAuthorizedHosts: doNotFlushAuthorizedHosts, 50 | } 51 | 52 | c, err := conntrack.Dial(nil) 53 | if err != nil { 54 | return nil, nil, err 55 | } 56 | 57 | authorizer.fwl.Debug("Opening Conntrack channel") 58 | authorizer.conntrack = c 59 | 60 | if authorizer.ttlCheckTicker < 1 { 61 | return nil, nil, fmt.Errorf(errTTL) 62 | } else if authorizer.ttlCheckTicker < 30 { 63 | authorizer.fwl.Warnf(warnTTL, authorizer.ttlCheckTicker) 64 | } 65 | 66 | if authorizer.authorizedSet == "" { 67 | return nil, nil, fmt.Errorf(errSetName) 68 | } 69 | 70 | cacheContext, err := authorizer.ttlCacheChecker() 71 | if err != nil { 72 | return nil, nil, err 73 | } 74 | 75 | hosts, err := authorizer.fw.GetIPv4AuthorizedHosts(authorizedSet) 76 | if err != nil { 77 | log.Fatal(err) 78 | } 79 | if len(hosts) > 0 { 80 | for _, h := range hosts { 81 | authorizer.fwl.Debugf( 82 | "Found host %s in %s set. Importing into cache", 83 | h, 84 | authorizedSet, 85 | ) 86 | authorizer.cache.Register(h.String()) 87 | } 88 | } 89 | 90 | return authorizer, cacheContext, nil 91 | } 92 | -------------------------------------------------------------------------------- /firewall/nftables/nft.go: -------------------------------------------------------------------------------- 1 | package nftables 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/google/nftables" 7 | ) 8 | 9 | // FirewallBackend for nftables 10 | type FirewallBackend struct { 11 | sync.Mutex 12 | nft *nftables.Conn 13 | tableName, chainName string 14 | table *nftables.Table 15 | chain *nftables.Chain 16 | } 17 | 18 | // NewFirewallBackend for creating a new nftables FirewaBackend 19 | func NewFirewallBackend(hook, table, chain string) (*FirewallBackend, error) { 20 | firewallBackend := &FirewallBackend{ 21 | nft: &nftables.Conn{}, 22 | tableName: table, 23 | chainName: chain, 24 | } 25 | 26 | // HookType [OUTPUT/FORWARD] 27 | var hT nftables.ChainHook 28 | // ChainType [FILTER]. We may add more ChainTypes in the future 29 | // but for now NetFilter will handle only Host Outbound requests 30 | // on FILTER/OUTPUT or for intermediate gateways on FILTER/FORWARD 31 | var cT nftables.ChainType 32 | switch hook { 33 | case "OUTPUT": 34 | hT = nftables.ChainHookOutput 35 | cT = nftables.ChainTypeFilter 36 | case "FORWARD": 37 | hT = nftables.ChainHookForward 38 | cT = nftables.ChainTypeFilter 39 | default: 40 | hT = nftables.ChainHookOutput 41 | cT = nftables.ChainTypeFilter 42 | } 43 | 44 | err := firewallBackend.CreateIPv4Table(table) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | nt, err := firewallBackend.getTable(table) 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | err = firewallBackend.CreateIPv4Chain( 55 | table, 56 | chain, 57 | string(cT), 58 | int(hT), 59 | ) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | nc, err := firewallBackend.getChain(chain) 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | firewallBackend.table = nt 70 | firewallBackend.chain = nc 71 | 72 | return firewallBackend, nil 73 | } 74 | 75 | // DropIPv4Input for creating FILTER/INPUT chain that drops all inbound traffic except 76 | // loopback traffic or established,related traffic 77 | func (f *FirewallBackend) DropIPv4Input(table, chain string) error { 78 | err := f.CreateIPv4Table(table) 79 | if err != nil { 80 | return err 81 | } 82 | 83 | nti, err := f.getTable(table) 84 | if err != nil { 85 | return err 86 | } 87 | 88 | err = f.CreateIPv4Chain( 89 | table, 90 | "input", 91 | string(nftables.ChainTypeFilter), 92 | int(nftables.ChainHookInput), 93 | ) 94 | if err != nil { 95 | return err 96 | } 97 | 98 | nci, err := f.getChain("input") 99 | if err != nil { 100 | return err 101 | } 102 | 103 | err = f.createChainInputWithEstablished(nti, nci) 104 | if err != nil { 105 | return err 106 | } 107 | 108 | return nil 109 | } 110 | -------------------------------------------------------------------------------- /firewall/firewall.go: -------------------------------------------------------------------------------- 1 | package firewall 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "strings" 7 | 8 | "github.com/sirupsen/logrus" 9 | "github.com/ulfox/nettrust/firewall/nftables" 10 | ) 11 | 12 | // backend interface for implementing different firewall backends. nftables, iptables, iptables-nft 13 | type backend interface { 14 | AddIPv4Rule(ip string) error 15 | DeleteIPv4Rule(ip string) error 16 | AddIPv4NetworkRule(cidr string) error 17 | DeleteIPv4NetworkRule(cidr string) error 18 | AddIPv4Set(n string) error 19 | AddIPv4SetRule(n string) error 20 | AddIPv4ToSetRule(n, ip string) error 21 | DeleteIPv4FromAuthorizedList(n, ip string) error 22 | AddTailingReject() error 23 | FlushTable(t string) error 24 | DeleteChain(c string) error 25 | DeleteTable(t string) error 26 | GetIPv4AuthorizedHosts(s string) ([]net.IP, error) 27 | CreateIPv4Table(t string) error 28 | CreateIPv4Chain(t, c, ct string, ht int) error 29 | DropIPv4Input(t, c string) error 30 | } 31 | 32 | // Firewall for managing firewall rules 33 | type Firewall struct { 34 | logger *logrus.Logger 35 | ingress chan net.IP 36 | backend 37 | table, chain string 38 | } 39 | 40 | func (f *Firewall) backendExecutor(b, h string) (*backend, error) { 41 | var beE backend 42 | 43 | if h != "OUTPUT" && h != "FORWARD" { 44 | return nil, fmt.Errorf(errFWDHook, h) 45 | } 46 | 47 | if b == "nftables" { 48 | nft, err := nftables.NewFirewallBackend(h, f.table, f.chain) 49 | if err != nil { 50 | return nil, err 51 | } 52 | beE = nft 53 | 54 | return &beE, nil 55 | } 56 | 57 | if b == "iptables" || b == "iptables-nft" { 58 | return nil, fmt.Errorf(errNotSupportedFWDBackend, b) 59 | } 60 | 61 | return nil, fmt.Errorf(errUnknownFWDBackend, b) 62 | } 63 | 64 | // NewFirewall for creating a new firewall. 65 | // Params: backend = nftables/iptables/iptables-nft. 66 | // hook = OUTPUT/FORWARD. 67 | // table = table name that will be used/created (nftables). 68 | // chain = chain name that will be created. 69 | func NewFirewall( 70 | backend, hook, table, chain string, 71 | dropInput bool, 72 | logger *logrus.Logger, 73 | ) (*Firewall, error) { 74 | 75 | if table == "" { 76 | return nil, fmt.Errorf(errEmptyName, "table") 77 | } 78 | 79 | if chain == "" { 80 | return nil, fmt.Errorf(errEmptyName, "chain") 81 | } 82 | 83 | fw := &Firewall{ 84 | logger: logger, 85 | ingress: make(chan net.IP), 86 | table: table, 87 | chain: chain, 88 | } 89 | 90 | log := fw.logger.WithFields(logrus.Fields{ 91 | "Component": "Firewall", 92 | "Stage": "Configure", 93 | }) 94 | 95 | log.Infof(infoFWDCreate, hook) 96 | beE, err := fw.backendExecutor(backend, strings.ToUpper(hook)) 97 | if err != nil { 98 | return nil, err 99 | } 100 | 101 | fw.backend = *beE 102 | 103 | if dropInput { 104 | log.Info(infoFWDInput) 105 | err = fw.DropIPv4Input(table, chain) 106 | if err != nil { 107 | return nil, err 108 | } 109 | } 110 | 111 | return fw, nil 112 | } 113 | -------------------------------------------------------------------------------- /firewall/nftables/nft4net.go: -------------------------------------------------------------------------------- 1 | package nftables 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | "net" 7 | "strings" 8 | 9 | "github.com/google/nftables" 10 | "github.com/google/nftables/expr" 11 | ) 12 | 13 | func (f *FirewallBackend) getIPv4NetworkRule(cidr string) (*nftables.Rule, error) { 14 | _, n, err := net.ParseCIDR(cidr) 15 | if err != nil { 16 | return nil, err 17 | } 18 | 19 | f.Lock() 20 | rules, err := f.nft.GetRule(f.table, f.chain) 21 | f.Unlock() 22 | 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | for _, rule := range rules { 28 | for _, e := range rule.Exprs { 29 | cmp, ok := e.(*expr.Cmp) 30 | if !ok { 31 | continue 32 | } 33 | 34 | if net.IP(cmp.Data).String() != n.IP.String() { 35 | continue 36 | } 37 | 38 | goto checkBitwise 39 | } 40 | continue 41 | checkBitwise: 42 | for _, bw := range rule.Exprs { 43 | bitwise, ok := bw.(*expr.Bitwise) 44 | if !ok { 45 | continue 46 | } 47 | if hex.EncodeToString(bitwise.Mask) == n.Mask.String() { 48 | return rule, nil 49 | } 50 | } 51 | } 52 | 53 | return nil, fmt.Errorf(errNotSuchIPv4NetRule, cidr) 54 | } 55 | 56 | // AddIPv4NetworkRule for whitelisting an IPv4 network in the chain. Should be used on initial setup and 57 | // sometimes for whitelisting new networks in the chain 58 | func (f *FirewallBackend) AddIPv4NetworkRule(cidr string) error { 59 | _, n, err := net.ParseCIDR(cidr) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | _, err = f.getIPv4NetworkRule(cidr) 65 | if err != nil { 66 | if !strings.HasPrefix(err.Error(), "could not find network rule with cidr") { 67 | return err 68 | } 69 | } else { 70 | return nil 71 | } 72 | 73 | f.Lock() 74 | defer f.Unlock() 75 | 76 | f.nft.AddRule(&nftables.Rule{ 77 | Table: f.table, 78 | Chain: f.chain, 79 | Exprs: []expr.Any{ 80 | &expr.Payload{ 81 | OperationType: expr.PayloadLoad, 82 | DestRegister: 1, 83 | Base: expr.PayloadBaseNetworkHeader, 84 | Offset: 16, 85 | Len: 4, 86 | }, 87 | &expr.Bitwise{ 88 | SourceRegister: 1, 89 | DestRegister: 1, 90 | Len: 4, 91 | Mask: n.Mask, 92 | Xor: []byte{0x00, 0x00, 0x00, 0x00}, 93 | }, 94 | &expr.Cmp{ 95 | Op: expr.CmpOpEq, 96 | Register: 1, 97 | Data: n.IP, 98 | }, 99 | &expr.Counter{}, 100 | &expr.Verdict{Kind: expr.VerdictAccept}, 101 | }, 102 | }) 103 | return f.nft.Flush() 104 | } 105 | 106 | // DeleteIPv4NetworkRule for deleting a whitelisted network from the chain 107 | func (f *FirewallBackend) DeleteIPv4NetworkRule(cidr string) error { 108 | _, _, err := net.ParseCIDR(cidr) 109 | if err != nil { 110 | return err 111 | } 112 | 113 | r, err := f.getIPv4NetworkRule(cidr) 114 | if err != nil { 115 | if !strings.HasPrefix(err.Error(), "could not find network rule with cidr") { 116 | return err 117 | } 118 | return nil 119 | } 120 | 121 | r.Chain = f.chain 122 | r.Table = f.table 123 | 124 | f.Lock() 125 | defer f.Unlock() 126 | 127 | err = f.nft.DelRule(r) 128 | if err != nil { 129 | return err 130 | } 131 | return f.nft.Flush() 132 | } 133 | -------------------------------------------------------------------------------- /authorizer/activity.go: -------------------------------------------------------------------------------- 1 | package authorizer 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | "time" 8 | 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | // ServiceContext for canceling goroutins 13 | type ServiceContext struct { 14 | cancel context.CancelFunc 15 | wg *sync.WaitGroup 16 | } 17 | 18 | // Expire will call cancel to terminate a context immediately, causing the goroutine to exit 19 | func (f *ServiceContext) Expire() { 20 | f.cancel() 21 | } 22 | 23 | // Wait ensures that the goroutine has exit successfully 24 | func (f *ServiceContext) Wait() { 25 | f.wg.Wait() 26 | } 27 | 28 | // ttlCacheChecker spawns a goroutine for checking cache for authorized hosts with expired ttl 29 | func (f *Authorizer) ttlCacheChecker() (*ServiceContext, error) { 30 | if f.fwl == nil { 31 | return nil, fmt.Errorf(errNil) 32 | } 33 | 34 | firewallCacheContext := &ServiceContext{} 35 | 36 | var serviceWG sync.WaitGroup 37 | firewallCacheContext.wg = &serviceWG 38 | 39 | ctx, cancel := context.WithCancel(context.Background()) 40 | firewallCacheContext.cancel = cancel 41 | 42 | serviceWG.Add(1) 43 | go func(ctx context.Context, wg *sync.WaitGroup, l *logrus.Entry) { 44 | ticker := time.NewTicker(time.Duration(f.ttlCheckTicker) * time.Second) 45 | for { 46 | select { 47 | case <-ctx.Done(): 48 | l := l.WithFields(logrus.Fields{ 49 | "Component": "Firewall", 50 | "Stage": "Deauthorize", 51 | }) 52 | 53 | if !f.doNotFlushAuthorizedHosts { 54 | for h := range f.cache.Hosts { 55 | l.Infof("Removing host [%s] from firewall rules", h) 56 | err := f.fw.DeleteIPv4FromAuthorizedList(f.authorizedSet, h) 57 | if err != nil { 58 | l.Error(err) 59 | } 60 | f.cache.Delete(h) 61 | } 62 | } 63 | 64 | l.Debug("Closing Conntrack channel") 65 | f.conntrack.Close() 66 | 67 | l.Info("Bye!") 68 | wg.Done() 69 | 70 | return 71 | case <-ticker.C: 72 | if f.cache.TTL < 0 { 73 | break 74 | } 75 | l.Debug("Gathering active hosts from conntrack") 76 | activeHosts, err := f.conntrackDump() 77 | if err != nil { 78 | l.Error(err) 79 | continue 80 | } 81 | 82 | // Blocking call. If the expired hosts or cache is very big we may get dns bottleneck. 83 | // During f.cache.Expired() call, RequestHandler will not be able to serve dns requests 84 | l.Debug("Checking cache for expired hosts") 85 | for _, h := range f.cache.Expired() { 86 | _, ok := activeHosts[h] 87 | if ok { 88 | l.Debugf("Host [%s] has expired but is stil active. Renewing", h) 89 | f.cache.Renew(h) 90 | continue 91 | } 92 | 93 | // Blocking call, but we expect this to be fast to mitigate any wait that 94 | // RequestHandler may encounter 95 | l.Debugf("Host [%s] has expired. Removing from firewall rules", h) 96 | err := f.fw.DeleteIPv4FromAuthorizedList(f.authorizedSet, h) 97 | if err != nil { 98 | l.Error(err) 99 | } 100 | 101 | // Blocking call, should be fast and not cause any delays to RequestHandler 102 | l.Debugf("Deleting host [%s] from cache", h) 103 | f.cache.Delete(h) 104 | delete(activeHosts, h) 105 | } 106 | l.Debugf("Freeing up Authorizer Cache Memory") 107 | f.cache.NewAuthMap() 108 | activeHosts = nil 109 | default: 110 | time.Sleep(time.Millisecond * 50) 111 | } 112 | } 113 | }(ctx, &serviceWG, f.fwl) 114 | 115 | return firewallCacheContext, nil 116 | } 117 | -------------------------------------------------------------------------------- /core/flags.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import "flag" 4 | 5 | var ( 6 | doNotFlushTable *bool 7 | doNotFlushAuthorizedHosts, fwdTLS *bool 8 | fwdAddr, fwdProto, fwdTLSCert *string 9 | fwdUDPBufferSize *uint16 10 | listenAddr, listenCert, listenCertKey *string 11 | listenTLS *bool 12 | 13 | firewallBackend, firewallType *string 14 | firewallDropInput *bool 15 | 16 | whitelistLoopback, whitelistPrivate *bool 17 | 18 | authorizedTTL, ttlCheckTicker *int 19 | 20 | fileCFG *string 21 | 22 | dnsTTLCache *int 23 | ) 24 | 25 | func init() { 26 | doNotFlushTable = flag.Bool( 27 | "do-not-flush-table", 28 | false, 29 | "Do not clean up tables when NetTrust exists. Use this flag if you want to continue to deny communication when NetTrust has exited", 30 | ) 31 | doNotFlushAuthorizedHosts = flag.Bool( 32 | "do-not-flush-authorized-hosts", 33 | false, 34 | "Do not clean up the authorized hosts list on exit. Use this together with do-not-flush-table to keep the NetTrust table as is on exit", 35 | ) 36 | 37 | fwdAddr = flag.String("fwd-addr", "", "NetTrust forward dns address") 38 | fwdProto = flag.String("fwd-proto", "", "NetTrust dns forward protocol") 39 | fwdTLS = flag.Bool( 40 | "fwd-tls", 41 | false, 42 | "Enable DoT. This expects that forward dns address supports DoT and fwd-proto is tcp", 43 | ) 44 | fwdTLSCert = flag.String( 45 | "fwd-tls-cert", 46 | "", 47 | "path to certificate that will be used to validate forward dns hostname. If you do not set this, the the host root CAs will be used", 48 | ) 49 | 50 | ubs := uint16(*flag.Uint("fwd-udp-buffer-size", 0, "forward client udp buffer size, default 4096")) 51 | fwdUDPBufferSize = &ubs 52 | 53 | listenAddr = flag.String("listen-addr", "", "NetTrust listen dns address") 54 | listenTLS = flag.Bool("listen-tls", false, "Enable tls listener, tls listener works only with the TCP DNS Service, UDP will continue to serve in plaintext mode") 55 | listenCert = flag.String("listen-cert", "", "path to certificate that will be used by the TCP DNS Service to serve DoT") 56 | listenCertKey = flag.String("listen-cert-key", "", "path to the private key that will be used by the TCP DNS Service to serve DoT") 57 | 58 | firewallBackend = flag.String( 59 | "firewall-backend", 60 | "", 61 | "NetTrust firewall backend [nftables/iptables/iptables-nft] that will be used to interact with Netfilter (nftables is only supported for now)", 62 | ) 63 | firewallType = flag.String( 64 | "firewall-type", 65 | "", 66 | "NetTrust firewall type. Supported types: OUTPUT (default), FORWARD. The type essentially tells NetTrust on which hook the rules will be added", 67 | ) 68 | firewallDropInput = flag.Bool( 69 | "firewall-drop-input", 70 | false, 71 | "If enabled, NetTrust will drop input. Adds [ct state established,related accept] & ['lo' accept]. Should be enabled only when NetTrust runs in host", 72 | ) 73 | 74 | whitelistLoopback = flag.Bool( 75 | "whitelist-loopback", 76 | true, 77 | "Loopback network space 127.0.0.0/8 will be whitelisted (default true)", 78 | ) 79 | whitelistPrivate = flag.Bool( 80 | "whitelist-private", 81 | true, 82 | "If 10.0.0.0/8, 172.16.0.0/16, 192.168.0.0/16, 100.64.0.0/10 will be whitelisted (default true)", 83 | ) 84 | 85 | authorizedTTL = flag.Int( 86 | "authorized-ttl", 87 | 0, 88 | "Number of seconds a authorized host will be active before NetTrust expires it and expect a DNS query again (-1 do not expire)", 89 | ) 90 | ttlCheckTicker = flag.Int( 91 | "ttl-check-ticker", 92 | 0, 93 | "How often NetTrust should check the cache for expired authorized hosts (Checking is blocking, do not put small numbers)", 94 | ) 95 | 96 | fileCFG = flag.String("config", "", "Path to config.json") 97 | 98 | dnsTTLCache = flag.Int("dns-ttl-cache", 0, "Number of seconds dns queries stay in cache (-1 to disable caching)") 99 | 100 | } 101 | -------------------------------------------------------------------------------- /authorizer/handler.go: -------------------------------------------------------------------------------- 1 | package authorizer 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "strings" 7 | 8 | "github.com/miekg/dns" 9 | ) 10 | 11 | // HandleRequest for filtering dns respone requests 12 | func (f *Authorizer) HandleRequest(resp *dns.Msg) error { 13 | if f.cache == nil { 14 | return fmt.Errorf(errNil) 15 | } 16 | 17 | question := resp.Question[0].Name 18 | 19 | if resp.Rcode == dns.RcodeNameError { 20 | f.fwl.Infof(infoNXDomain, question) 21 | return nil 22 | } 23 | 24 | if resp.Rcode != dns.RcodeSuccess { 25 | return fmt.Errorf(errRcode, question, resp.Rcode) 26 | } 27 | 28 | if len(resp.Answer) == 0 { 29 | if resp.Question[0].Qtype == dns.TypeAAAA { 30 | f.fwl.Infof(infoAuthIPv6Block, question) 31 | return nil 32 | } 33 | f.fwl.Infof(infoAuthBlock, question) 34 | return nil 35 | } 36 | 37 | if resp.Question[0].Qtype == dns.TypeA { 38 | for _, answer := range resp.Answer { 39 | if _, ok := answer.(*dns.CNAME); ok { 40 | // Nothing to do here for now. CNAME is not IP Address 41 | // this should not be a problem since usually CNAME 42 | // addresses are answered in the same query 43 | continue 44 | } 45 | 46 | r, ok := answer.(*dns.A) 47 | if !ok { 48 | f.fwl.Errorf(errInvalidReply, "TypeA", question) 49 | continue 50 | } 51 | 52 | err := f.authIPv4(question, r.A.String()) 53 | if err != nil { 54 | f.fwl.Error(err) 55 | } 56 | } 57 | 58 | return nil 59 | } 60 | 61 | if resp.Question[0].Qtype == dns.TypeAAAA { 62 | for _, answer := range resp.Answer { 63 | if r, ok := answer.(*dns.AAAA); ok { 64 | // Not supported yet 65 | f.fwl.Warnf(warnIPv6Support, question, r.AAAA.String()) 66 | } 67 | } 68 | 69 | return nil 70 | } 71 | 72 | if resp.Question[0].Qtype == dns.TypePTR { 73 | answerSlice := []string{} 74 | for _, answer := range resp.Answer { 75 | r, ok := answer.(*dns.PTR) 76 | if !ok { 77 | f.fwl.Errorf(errInvalidReply, "TypePTR", question) 78 | continue 79 | } 80 | answerSlice = append(answerSlice, r.Ptr) 81 | } 82 | 83 | if t := strings.Split(question, ".arpa")[0]; strings.HasSuffix(t, ".ip6") { 84 | f.fwl.Warnf(warnPTRIPv6, question, strings.Join(answerSlice, " ")) 85 | return nil 86 | } 87 | 88 | // Split PTR Question BE IPv4 string 89 | revAddr := strings.Split(question, ".in-addr.arpa")[0] 90 | 91 | // Split BE IPv4 string into a slice 92 | revAddrSlice := strings.Split(revAddr, ".") 93 | addrSlice := []string{} 94 | 95 | // Convert to LE 96 | for i := len(revAddrSlice) - 1; i >= 0; i-- { 97 | addrSlice = append(addrSlice, revAddrSlice[i]) 98 | } 99 | 100 | // Construct back IPv4 into LE 101 | addr := strings.Join(addrSlice, ".") 102 | f.fwl.Infof(infoPTRIPv4, question, addr, strings.Join(answerSlice, "")) 103 | 104 | err := f.authIPv4(question, addr) 105 | if err != nil { 106 | f.fwl.Error(err) 107 | } 108 | 109 | return nil 110 | } 111 | 112 | f.fwl.Warnf(warnNotSupportedQuery, resp.Question[0].Qtype, question) 113 | 114 | return nil 115 | } 116 | 117 | func (f *Authorizer) authIPv4(question, ip string) error { 118 | blacklisted, err := f.checkIPv4Blacklist(ip) 119 | if err != nil { 120 | return err 121 | } 122 | 123 | if blacklisted { 124 | f.fwl.Infof(infoAuthBlacklist, question, ip) 125 | return nil 126 | } 127 | 128 | if ip == "0.0.0.0" { 129 | f.fwl.Infof(infoAuthBlock, question) 130 | return nil 131 | } 132 | 133 | regOK := f.cache.Register(ip) 134 | if !regOK { 135 | f.cache.Renew(ip) 136 | f.fwl.Infof(infoAuthExists, question, ip) 137 | return nil 138 | } 139 | 140 | err = f.fw.AddIPv4ToSetRule(f.authorizedSet, ip) 141 | if err != nil { 142 | return err 143 | } 144 | f.fwl.Infof(infoAuth, question, ip) 145 | 146 | return nil 147 | } 148 | 149 | func (f *Authorizer) checkIPv4Blacklist(ip string) (bool, error) { 150 | // unsafe function, we are not checking string input for valid 151 | // ip address. We should add a check here 152 | for _, j := range f.blacklistHosts { 153 | if ip == j { 154 | return true, nil 155 | } 156 | } 157 | 158 | for _, j := range f.blacklistNetworks { 159 | _, netj, err := net.ParseCIDR(j) 160 | if err != nil { 161 | return false, err 162 | } 163 | 164 | if netj.Contains(net.ParseIP(ip)) { 165 | return true, nil 166 | } 167 | } 168 | 169 | return false, nil 170 | } 171 | -------------------------------------------------------------------------------- /dns/dns.go: -------------------------------------------------------------------------------- 1 | package dns 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "crypto/x509" 7 | "fmt" 8 | "io/ioutil" 9 | "net" 10 | "os" 11 | "sync" 12 | "syscall" 13 | 14 | "github.com/miekg/dns" 15 | "github.com/sirupsen/logrus" 16 | qc "github.com/ulfox/nettrust/dns/cache" 17 | ) 18 | 19 | // Server defines NetTrust DNS Server proxy. The server invokes firewall calls also 20 | type Server struct { 21 | sync.Mutex 22 | 23 | listenAddr, fwdAddr, fwdProto string 24 | listenTLS, fwdTLS, sigOnce bool 25 | dnsTTLCache int 26 | clientUDPBufferSize uint16 27 | listenCerts *tls.Certificate 28 | logger *logrus.Logger 29 | fwdl *logrus.Entry 30 | client *dns.Client 31 | udpServer, tcpServer *dns.Server 32 | cancelOnErr context.CancelFunc 33 | ctxOnErr context.Context 34 | cache *qc.Queries 35 | cacheContext *ServiceContext 36 | domainBlacklist map[string]struct{} 37 | } 38 | 39 | // NewDNSServer for creating a new NetTrust DNS Server proxy 40 | func NewDNSServer( 41 | laddr, faddr, fwdProto, listenCert, ListenCertKey, clientCaCert string, 42 | listenTLS, fwdTLS bool, 43 | dnsTTLCache int, 44 | clientUDPBufferSize uint16, 45 | domainBlacklist []string, 46 | logger *logrus.Logger, 47 | ) (*Server, error) { 48 | 49 | if laddr == "" || faddr == "" { 50 | return nil, fmt.Errorf(errFWDNSAddr) 51 | } 52 | 53 | if fwdTLS && fwdProto != "tcp" { 54 | return nil, fmt.Errorf(errFWDTLS) 55 | } 56 | 57 | if fwdProto != "udp" && fwdProto != "tcp" { 58 | return nil, fmt.Errorf(errFWDNSProto) 59 | } 60 | 61 | host, port, err := net.SplitHostPort(faddr) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | if host == "" || port == "" { 67 | return nil, fmt.Errorf(errFWDNSAddrInvalid, host, port) 68 | } 69 | 70 | if fwdTLS && port == "53" { 71 | logger.Warn(warnFWDTLSPort) 72 | } 73 | 74 | client := &dns.Client{Net: fwdProto, UDPSize: uint16(clientUDPBufferSize)} 75 | // client := &dns.Client{Net: fwdProto} 76 | if fwdTLS { 77 | client.Net = "tcp-tls" 78 | } 79 | 80 | dB := make(map[string]struct{}, len(domainBlacklist)) 81 | 82 | for _, j := range domainBlacklist { 83 | dB[j] = struct{}{} 84 | } 85 | 86 | if fwdTLS && clientCaCert != "" { 87 | certPool, err := loadClientCaCert(clientCaCert) 88 | if err != nil { 89 | return nil, err 90 | } 91 | client.TLSConfig = &tls.Config{ 92 | RootCAs: certPool, 93 | } 94 | } 95 | 96 | server := &Server{ 97 | listenAddr: laddr, 98 | listenTLS: listenTLS, 99 | fwdAddr: faddr, 100 | fwdProto: fwdProto, 101 | fwdTLS: fwdTLS, 102 | dnsTTLCache: dnsTTLCache, 103 | logger: logger, 104 | client: client, 105 | cache: qc.NewCache(dnsTTLCache), 106 | domainBlacklist: dB, 107 | } 108 | 109 | if listenTLS { 110 | cert, err := tls.LoadX509KeyPair(listenCert, ListenCertKey) 111 | if err != nil { 112 | return nil, err 113 | } 114 | server.listenCerts = &cert 115 | } 116 | 117 | server.fwdl = server.logger.WithFields(logrus.Fields{ 118 | "Component": "DNS Server", 119 | "Stage": "Forward", 120 | }) 121 | 122 | server.ctxOnErr, server.cancelOnErr = context.WithCancel(context.Background()) 123 | 124 | server.cacheContext, err = server.dnsTTLCacheManager() 125 | if err != nil { 126 | return nil, err 127 | } 128 | 129 | return server, nil 130 | } 131 | 132 | func loadClientCaCert(ca string) (*x509.CertPool, error) { 133 | f, err := os.Stat(ca) 134 | if os.IsNotExist(err) { 135 | return nil, err 136 | } 137 | 138 | if f.IsDir() { 139 | return nil, fmt.Errorf(errNotAFile, ca) 140 | } 141 | 142 | data, err := ioutil.ReadFile(ca) 143 | if err != nil { 144 | return nil, err 145 | } 146 | 147 | certPool := x509.NewCertPool() 148 | certPool.AppendCertsFromPEM(data) 149 | 150 | return certPool, nil 151 | } 152 | 153 | func (s *Server) killOnErr() { 154 | if s.sigOnce { 155 | return 156 | } 157 | 158 | s.Lock() 159 | s.sigOnce = true 160 | s.Unlock() 161 | 162 | s.fwdl.Error("nettrust is shutting down, sending SIGINT") 163 | err := syscall.Kill(syscall.Getegid(), syscall.SIGINT) 164 | if err != nil { 165 | s.fwdl.Error(err) 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /firewall/nftables/nft4set.go: -------------------------------------------------------------------------------- 1 | package nftables 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "strings" 7 | 8 | "github.com/google/nftables" 9 | "github.com/google/nftables/expr" 10 | ) 11 | 12 | func (f *FirewallBackend) getIPv4Set(n string) (*nftables.Set, error) { 13 | f.Lock() 14 | set, err := f.nft.GetSetByName(f.table, n) 15 | f.Unlock() 16 | 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | return set, nil 22 | } 23 | 24 | func (f *FirewallBackend) getIPv4SetRule(n string) (*nftables.Rule, error) { 25 | f.Lock() 26 | rules, err := f.nft.GetRule(f.table, f.chain) 27 | f.Unlock() 28 | 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | for _, rule := range rules { 34 | for _, e := range rule.Exprs { 35 | lookup, ok := e.(*expr.Lookup) 36 | if !ok { 37 | continue 38 | } 39 | 40 | if lookup.SetName != n { 41 | continue 42 | } 43 | 44 | return rule, nil 45 | } 46 | } 47 | 48 | return nil, fmt.Errorf(errNoSuchIPv4SetRule, n) 49 | } 50 | 51 | // GetIPv4AuthorizedHosts return a slice of all hosts from a set 52 | func (f *FirewallBackend) GetIPv4AuthorizedHosts(s string) ([]net.IP, error) { 53 | set, err := f.getIPv4Set(s) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | f.Lock() 59 | elements, err := f.nft.GetSetElements(set) 60 | f.Unlock() 61 | 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | var hosts []net.IP 67 | 68 | for _, e := range elements { 69 | hosts = append(hosts, e.Key) 70 | } 71 | 72 | return hosts, nil 73 | } 74 | 75 | // AddIPv4Set for adding a new IPv4 set in the chain 76 | func (f *FirewallBackend) AddIPv4Set(n string) error { 77 | _, err := f.getIPv4Set(n) 78 | if err == nil { 79 | return nil 80 | } 81 | 82 | f.Lock() 83 | defer f.Unlock() 84 | 85 | set := &nftables.Set{ 86 | Name: n, 87 | Anonymous: false, 88 | Interval: false, 89 | Table: f.table, 90 | KeyType: nftables.TypeIPAddr, 91 | } 92 | err = f.nft.AddSet(set, []nftables.SetElement{}) 93 | if err != nil { 94 | return err 95 | } 96 | 97 | return f.nft.Flush() 98 | } 99 | 100 | // AddIPv4SetRule for adding a whitelist rule in the chain for a specific IPv4 set 101 | func (f *FirewallBackend) AddIPv4SetRule(n string) error { 102 | set, err := f.getIPv4Set(n) 103 | if err != nil { 104 | return err 105 | } 106 | 107 | _, err = f.getIPv4SetRule(n) 108 | if err != nil { 109 | if !strings.HasPrefix(err.Error(), "could not find set rule with name") { 110 | return err 111 | } 112 | } else { 113 | return nil 114 | } 115 | 116 | f.Lock() 117 | defer f.Unlock() 118 | 119 | f.nft.AddRule(&nftables.Rule{ 120 | Table: f.table, 121 | Chain: f.chain, 122 | Exprs: []expr.Any{ 123 | &expr.Payload{ 124 | DestRegister: 1, 125 | Base: expr.PayloadBaseNetworkHeader, 126 | Offset: 16, 127 | Len: 4, 128 | }, 129 | &expr.Lookup{ 130 | SourceRegister: 1, 131 | SetName: set.Name, 132 | SetID: set.ID, 133 | }, 134 | &expr.Verdict{ 135 | Kind: expr.VerdictAccept, 136 | }, 137 | }, 138 | }) 139 | 140 | return f.nft.Flush() 141 | } 142 | 143 | // AddIPv4ToSetRule for adding a new IPv4 host in a set 144 | func (f *FirewallBackend) AddIPv4ToSetRule(n, ip string) error { 145 | set, err := f.getIPv4Set(n) 146 | if err != nil { 147 | return err 148 | } 149 | 150 | netIP := net.ParseIP(ip).To4() 151 | if netIP == nil { 152 | return fmt.Errorf(errNotValidIPv4Addr, ip) 153 | } 154 | 155 | f.Lock() 156 | defer f.Unlock() 157 | 158 | err = f.nft.SetAddElements( 159 | set, 160 | []nftables.SetElement{ 161 | { 162 | Key: netIP.To4(), 163 | }, 164 | }, 165 | ) 166 | if err != nil { 167 | return err 168 | } 169 | 170 | return f.nft.Flush() 171 | } 172 | 173 | // DeleteIPv4FromAuthorizedList for deleting an IPv4 host from a set 174 | func (f *FirewallBackend) DeleteIPv4FromAuthorizedList(n, ip string) error { 175 | set, err := f.getIPv4Set(n) 176 | if err != nil { 177 | return err 178 | } 179 | 180 | netIP := net.ParseIP(ip).To4() 181 | if netIP == nil { 182 | return fmt.Errorf(errNotValidIPv4Addr, ip) 183 | } 184 | 185 | f.Lock() 186 | defer f.Unlock() 187 | 188 | err = f.nft.SetDeleteElements( 189 | set, 190 | []nftables.SetElement{ 191 | { 192 | Key: netIP.To4(), 193 | }, 194 | }, 195 | ) 196 | if err != nil { 197 | return err 198 | } 199 | 200 | return f.nft.Flush() 201 | } 202 | -------------------------------------------------------------------------------- /dns/proxy.go: -------------------------------------------------------------------------------- 1 | package dns 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/miekg/dns" 8 | ) 9 | 10 | func (s *Server) fwd(w dns.ResponseWriter, req *dns.Msg, fn func(resp *dns.Msg) error) { 11 | if len(req.Question) == 0 { 12 | s.qErr(w, req, fmt.Errorf(errQuery)) 13 | return 14 | } 15 | 16 | if len(req.Question) > 1 { 17 | dns.HandleFailed(w, req) 18 | questions := []string{} 19 | 20 | for _, j := range req.Question { 21 | questions = append(questions, j.Name) 22 | } 23 | 24 | s.fwdl.Errorf(errManyQuestions, strings.Join(questions, " ")) 25 | return 26 | } 27 | 28 | question := s.cache.Question(req) 29 | 30 | if s.checkDomainBlacklist(question) { 31 | dns.HandleFailed(w, req) 32 | s.fwdl.Infof(infoDomainBlacklist, question) 33 | return 34 | } 35 | 36 | var resp *dns.Msg 37 | var err error 38 | 39 | if s.cache.GetTTL() <= 0 { 40 | goto forwardUpstream 41 | } 42 | 43 | // We need to handle IPv6 cache differently 44 | // Maybe a new cache or appening a string to 45 | // keep IPv6 records apart from IPv4 46 | if req.Question[0].Qtype == dns.TypeAAAA { 47 | goto forwardUpstream 48 | } 49 | 50 | if isCached := s.cache.Exists(question); isCached { 51 | if hasExpired := s.cache.HasExpired(question); hasExpired { 52 | s.cache.Delete(question) 53 | s.fwdl.Debugf(infoCacheObjExpired, question) 54 | goto forwardUpstream 55 | } 56 | 57 | r := s.cache.Get(question) 58 | if r != nil { 59 | s.fwdl.Debugf(infoCacheObjFound, question) 60 | resp = r 61 | resp.Id = req.Id 62 | goto tellClient 63 | } 64 | 65 | s.fwdl.Errorf(errCacheFetch, question) 66 | } 67 | 68 | if isNXCached := s.cache.ExistsNX(question); isNXCached { 69 | if hasExpired := s.cache.HasExpiredNX(question); !hasExpired { 70 | s.fwdl.Debugf(infoCacheObjFoundNil, question) 71 | resp = req 72 | goto tellClient 73 | } 74 | 75 | s.cache.DeleteNX(question) 76 | s.fwdl.Debugf(infoCacheObjExpired, question) 77 | } 78 | 79 | forwardUpstream: 80 | resp, _, err = s.client.Exchange(req, s.fwdAddr) 81 | if err != nil { 82 | s.qErr(w, req, err) 83 | return 84 | } 85 | 86 | if s.cache.GetTTL() > 0 { 87 | err = s.pushToCache(resp) 88 | if err != nil { 89 | s.fwdl.Error(err) 90 | } 91 | } 92 | 93 | tellClient: 94 | err = fn(resp) 95 | if err != nil { 96 | s.qErr(w, req, err) 97 | return 98 | } 99 | 100 | err = w.WriteMsg(resp) 101 | if err != nil { 102 | s.qErr(w, req, err) 103 | } 104 | } 105 | 106 | func (s *Server) qErr(w dns.ResponseWriter, req *dns.Msg, err error) { 107 | s.fwdl.Error(err) 108 | s.cache.RegisterNX(s.cache.Question(req)) 109 | dns.HandleFailed(w, req) 110 | } 111 | 112 | func (s *Server) pushToCache(msg *dns.Msg) error { 113 | // Do not register IPv6 (see commend at line ~43 for additional info) 114 | if msg.Question[0].Qtype == dns.TypeAAAA { 115 | return nil 116 | } 117 | 118 | if len(msg.Answer) == 0 { 119 | return s.registerNX(msg) 120 | } 121 | 122 | if len(msg.Answer) == 1 { 123 | if q4, ok := msg.Answer[0].(*dns.A); ok { 124 | if q4.A.String() == "0.0.0.0" { 125 | return s.registerNX(msg) 126 | } 127 | } 128 | 129 | if q6, ok := msg.Answer[0].(*dns.AAAA); ok { 130 | if q6.AAAA.String() == "::" { 131 | return s.registerNX(msg) 132 | } 133 | } 134 | } 135 | 136 | return s.register(msg) 137 | } 138 | 139 | func (s *Server) registerNX(msg *dns.Msg) error { 140 | q := s.cache.Question(msg) 141 | 142 | if exists := s.cache.ExistsNX(q); exists { 143 | if expired := s.cache.HasExpiredNX(q); expired { 144 | ok := s.cache.RenewNX(q) 145 | if !ok { 146 | return fmt.Errorf(errCacheCoulndNotRenew, q) 147 | } 148 | } 149 | return nil 150 | } 151 | 152 | ok := s.cache.RegisterNX(q) 153 | if !ok { 154 | return fmt.Errorf(errCacheRegister, q) 155 | } 156 | return nil 157 | } 158 | 159 | func (s *Server) register(msg *dns.Msg) error { 160 | q := s.cache.Question(msg) 161 | 162 | if exists := s.cache.Exists(q); exists { 163 | if expired := s.cache.HasExpired(q); expired { 164 | ok := s.cache.Renew(q, msg) 165 | if !ok { 166 | return fmt.Errorf(errCacheCoulndNotRenew, q) 167 | } 168 | } 169 | return nil 170 | } 171 | 172 | ok := s.cache.Register(q, msg) 173 | if !ok { 174 | return fmt.Errorf(errCacheRegister, q) 175 | } 176 | 177 | return nil 178 | } 179 | 180 | func (s *Server) checkDomainBlacklist(d string) bool { 181 | _, ok := s.domainBlacklist[d] 182 | return ok 183 | } 184 | -------------------------------------------------------------------------------- /dns/cache/cache.go: -------------------------------------------------------------------------------- 1 | package queries 2 | 3 | import ( 4 | "strings" 5 | "sync" 6 | "time" 7 | 8 | "github.com/miekg/dns" 9 | ) 10 | 11 | // Answered for storing dns replies with TTL 12 | type Answered struct { 13 | M *dns.Msg 14 | T time.Time 15 | } 16 | 17 | // Queries for storing dns answers 18 | type Queries struct { 19 | sync.Mutex 20 | ttl int 21 | resolved map[string]Answered 22 | nx map[string]time.Time 23 | } 24 | 25 | // NewCache creates a new empty cache 26 | func NewCache(ttl int) *Queries { 27 | return &Queries{ 28 | ttl: ttl, 29 | resolved: make(map[string]Answered), 30 | nx: make(map[string]time.Time), 31 | } 32 | } 33 | 34 | // NewResolved replaces current cache.resolved map with a new map 35 | // by copying all the elements. We this to free up memory, since 36 | // the allocated memory by cache.resolved is that that the map had at 37 | // its peak 38 | func (c *Queries) NewResolved() { 39 | c.Lock() 40 | defer c.Unlock() 41 | 42 | newMap := make(map[string]Answered) 43 | 44 | for k, v := range c.resolved { 45 | newMap[k] = v 46 | } 47 | 48 | c.resolved = nil 49 | c.resolved = newMap 50 | } 51 | 52 | // NewNX replaces current cache.nx map with a new map 53 | // by copying all the elements. See cache.NewResolved 54 | // for additional information 55 | func (c *Queries) NewNX() { 56 | c.Lock() 57 | defer c.Unlock() 58 | 59 | newMap := make(map[string]time.Time) 60 | 61 | for k, v := range c.nx { 62 | newMap[k] = v 63 | } 64 | 65 | c.nx = nil 66 | c.nx = newMap 67 | } 68 | 69 | // GetTTL return cache.ttl value 70 | func (c *Queries) GetTTL() int { 71 | return c.ttl 72 | } 73 | 74 | // Question returns dns.Msg.Question[0].Name from a given dns message 75 | func (c *Queries) Question(msg *dns.Msg) string { 76 | return strings.TrimSuffix(msg.Question[0].Name, ".") 77 | } 78 | 79 | // Exists (blocking) returns true if a question is in cache 80 | func (c *Queries) Exists(q string) bool { 81 | c.Lock() 82 | defer c.Unlock() 83 | 84 | _, ok := c.resolved[q] 85 | 86 | return ok 87 | } 88 | 89 | // ExistsNX (blocking) returns true if a question is in NX cache 90 | func (c *Queries) ExistsNX(q string) bool { 91 | c.Lock() 92 | defer c.Unlock() 93 | 94 | _, ok := c.nx[q] 95 | 96 | return ok 97 | } 98 | 99 | // HasExpired for checking if a specific question in cache has expired 100 | func (c *Queries) HasExpired(s string) bool { 101 | if !c.Exists(s) { 102 | return true 103 | } 104 | 105 | c.Lock() 106 | defer c.Unlock() 107 | 108 | q := c.resolved[s] 109 | 110 | return time.Since(q.T) > time.Second*time.Duration(c.ttl) 111 | } 112 | 113 | // HasExpiredNX for checking if a specific question in NX cache has expired 114 | func (c *Queries) HasExpiredNX(s string) bool { 115 | if !c.ExistsNX(s) { 116 | return true 117 | } 118 | 119 | c.Lock() 120 | defer c.Unlock() 121 | 122 | q := c.nx[s] 123 | 124 | return time.Since(q) > time.Second*time.Duration(c.ttl) 125 | } 126 | 127 | // Get for getting a cached question from the cache 128 | func (c *Queries) Get(s string) *dns.Msg { 129 | if c.Exists(s) { 130 | return c.resolved[s].M 131 | } 132 | 133 | return nil 134 | } 135 | 136 | // Register (blocking) for adding a new question to cache 137 | func (c *Queries) Register(s string, msg *dns.Msg) bool { 138 | if c.Exists(s) { 139 | return false 140 | } 141 | 142 | c.Lock() 143 | defer c.Unlock() 144 | 145 | c.resolved[s] = Answered{ 146 | M: msg, 147 | T: time.Now(), 148 | } 149 | 150 | return true 151 | } 152 | 153 | // RegisterNX (blocking) for adding a new question to NX cache 154 | func (c *Queries) RegisterNX(s string) bool { 155 | if c.ExistsNX(s) { 156 | return false 157 | } 158 | 159 | c.Lock() 160 | defer c.Unlock() 161 | 162 | c.nx[s] = time.Now() 163 | 164 | return true 165 | } 166 | 167 | // Renew (blocking) for updating ttl for a question in cache 168 | func (c *Queries) Renew(s string, msg *dns.Msg) bool { 169 | if !c.Exists(s) { 170 | return c.Register(s, msg) 171 | } 172 | 173 | c.Lock() 174 | defer c.Unlock() 175 | 176 | e := c.resolved[s] 177 | 178 | e.T = time.Now() 179 | 180 | return true 181 | } 182 | 183 | // RenewNX (blocking) for updating a ttl for a question in NX cache 184 | func (c *Queries) RenewNX(s string) bool { 185 | if !c.ExistsNX(s) { 186 | return c.RegisterNX(s) 187 | } 188 | 189 | c.Lock() 190 | defer c.Unlock() 191 | 192 | c.nx[s] = time.Now() 193 | 194 | return true 195 | } 196 | 197 | // ExpiredQueries (blocking) for returning all expired questions. Returns empty slice if c.ttl is < 0 198 | func (c *Queries) ExpiredQueries() []string { 199 | if c.ttl < 0 { 200 | return []string{} 201 | } 202 | 203 | c.Lock() 204 | defer c.Unlock() 205 | 206 | questions := []string{} 207 | for h, q := range c.resolved { 208 | if time.Since(q.T) > time.Second*time.Duration(c.ttl) { 209 | questions = append(questions, h) 210 | } 211 | } 212 | 213 | return questions 214 | } 215 | 216 | // ExpiredMXQueries (blocking) for returning all expired NX questions. Returns empty slice if c.ttl is < 0 217 | func (c *Queries) ExpiredMXQueries() []string { 218 | if c.ttl < 0 { 219 | return []string{} 220 | } 221 | 222 | c.Lock() 223 | defer c.Unlock() 224 | 225 | questions := []string{} 226 | for h, q := range c.nx { 227 | if time.Since(q) > time.Second*time.Duration(c.ttl) { 228 | questions = append(questions, h) 229 | } 230 | } 231 | 232 | return questions 233 | } 234 | 235 | // Delete (blocking) for deleting a question from cache 236 | func (c *Queries) Delete(h string) { 237 | c.Lock() 238 | defer c.Unlock() 239 | delete(c.resolved, h) 240 | } 241 | 242 | // DeleteNX (blocking) for deleting a question from NX cache 243 | func (c *Queries) DeleteNX(h string) { 244 | c.Lock() 245 | defer c.Unlock() 246 | delete(c.nx, h) 247 | } 248 | -------------------------------------------------------------------------------- /core/env.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "io/ioutil" 8 | "os" 9 | "strings" 10 | ) 11 | 12 | // NetTrust for reading either NET_TRUST env into a map or a config file into a map 13 | type NetTrust struct { 14 | Whitelist struct { 15 | Networks []string `json:"networks"` 16 | Hosts []string `json:"hosts"` 17 | } `json:"whitelist"` 18 | Blacklist struct { 19 | Networks []string `json:"networks"` 20 | Hosts []string `json:"hosts"` 21 | Domains []string `json:"domains"` 22 | } `json:"blacklist"` 23 | Env map[string]string 24 | DoNotFlushTable bool `json:"doNotFlushTable"` 25 | DoNotFlushAuthorizedHosts bool `json:"doNotFlushAuthorizedHosts"` 26 | FWDAddr string `json:"fwdAddr"` 27 | FWDProto string `json:"fwdProto"` 28 | FWDTLS bool `json:"fwdTLS"` 29 | FWDCaCert string `json:"fwdCaCert"` 30 | FWDUDPBufferSize uint16 `json:"fwdUDPBufferSize"` 31 | ListenAddr string `json:"listenAddr"` 32 | ListenTLS bool `json:"listenTLS"` 33 | ListenCert string `json:"listenCert"` 34 | ListenCertKey string `json:"listenCertKey"` 35 | FirewallBackend string `json:"firewallBackend"` 36 | FirewallType string `json:"firewallType"` 37 | FirewallDropInput bool `json:"firewallDropInput"` 38 | WhitelistLoEnabled bool `json:"whitelistLoEnabled"` 39 | WhitelistPrivateEnabled bool `json:"whitelistPrivateEnabled"` 40 | WhitelistLo []string 41 | WhitelistPrivate []string 42 | AuthorizedTTL int `json:"ttl"` 43 | TTLCheckTicker int `json:"ttlInterval"` 44 | DNSTTLCache int `json:"dnsTTLCache"` 45 | } 46 | 47 | // GetNetTrustEnv will read environ and create a map of k:v from envs 48 | // that have a NET_TRUST prefix. The prefix is removed 49 | func GetNetTrustEnv() (*NetTrust, error) { 50 | flag.Parse() 51 | 52 | var key string 53 | var err error 54 | env := make(map[string]string) 55 | osEnviron := os.Environ() 56 | NetTrustPrefix := "NET_TRUST_" 57 | for _, b := range osEnviron { 58 | if strings.HasPrefix(b, NetTrustPrefix) { 59 | pair := strings.SplitN(b, "=", 2) 60 | key = strings.TrimPrefix(pair[0], NetTrustPrefix) 61 | key = strings.ToLower(key) 62 | key = strings.Replace(key, "_", ".", -1) 63 | env[key] = pair[1] 64 | } 65 | } 66 | 67 | config := &NetTrust{} 68 | 69 | if *fileCFG != "" { 70 | err := fileExists(*fileCFG) 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | body, err := ioutil.ReadFile(*fileCFG) 76 | if err != nil { 77 | return nil, err 78 | } 79 | 80 | if err := json.Unmarshal(body, config); err != nil { 81 | return nil, err 82 | } 83 | } 84 | 85 | config.Env = env 86 | 87 | if *doNotFlushTable { 88 | config.DoNotFlushTable = *doNotFlushTable 89 | } 90 | 91 | if *doNotFlushAuthorizedHosts { 92 | config.DoNotFlushAuthorizedHosts = *doNotFlushAuthorizedHosts 93 | } 94 | 95 | if *fwdAddr != "" { 96 | config.FWDAddr = *fwdAddr 97 | } 98 | 99 | if *fwdProto == "" && config.FWDProto == "" { 100 | config.FWDProto = "udp" 101 | } else if *fwdProto != "" { 102 | config.FWDProto = *fwdProto 103 | } 104 | 105 | if *fwdTLS { 106 | config.FWDTLS = *fwdTLS 107 | } 108 | 109 | if *fwdTLSCert != "" { 110 | config.FWDCaCert = *fwdTLSCert 111 | } 112 | 113 | if config.FWDTLS && config.FWDCaCert != "" { 114 | err = fileExists(*fwdTLSCert) 115 | if err != nil { 116 | return nil, err 117 | } 118 | } 119 | 120 | if *fwdUDPBufferSize != 0 { 121 | config.FWDUDPBufferSize = *fwdUDPBufferSize 122 | } 123 | 124 | if *listenAddr != "" { 125 | config.ListenAddr = *listenAddr 126 | } 127 | 128 | if *listenTLS { 129 | config.ListenTLS = *listenTLS 130 | } 131 | 132 | if *listenCert != "" { 133 | config.ListenCert = *listenCert 134 | } 135 | 136 | if *listenCertKey != "" { 137 | config.ListenCertKey = *listenCertKey 138 | } 139 | 140 | if config.ListenTLS { 141 | if config.ListenCert == "" { 142 | return nil, fmt.Errorf(errListenTLSNoFile, "certificate") 143 | } 144 | if config.ListenCertKey == "" { 145 | return nil, fmt.Errorf(errListenTLSNoFile, "private key") 146 | } 147 | err = fileExists(config.ListenCert) 148 | if err != nil { 149 | return nil, err 150 | } 151 | err = fileExists(config.ListenCertKey) 152 | if err != nil { 153 | return nil, err 154 | } 155 | } 156 | 157 | if config.ListenAddr == config.FWDAddr { 158 | return nil, fmt.Errorf(errSameAddr) 159 | } 160 | 161 | if *firewallBackend == "" && config.FirewallBackend == "" { 162 | config.FirewallBackend = "nftables" 163 | } else if *firewallBackend != "" { 164 | config.FirewallBackend = *firewallBackend 165 | } 166 | 167 | if *firewallType == "" && config.FirewallType == "" { 168 | config.FirewallType = "OUTPUT" 169 | } else if *firewallType != "" { 170 | config.FirewallType = *firewallType 171 | } 172 | 173 | if *firewallDropInput { 174 | config.FirewallDropInput = *firewallDropInput 175 | } 176 | 177 | if *authorizedTTL == 0 && config.AuthorizedTTL == 0 { 178 | config.AuthorizedTTL = -1 179 | } else if *authorizedTTL != 0 { 180 | config.AuthorizedTTL = *authorizedTTL 181 | } 182 | 183 | if *ttlCheckTicker == 0 && config.TTLCheckTicker == 0 { 184 | config.TTLCheckTicker = 30 185 | } else if *ttlCheckTicker != 0 { 186 | config.TTLCheckTicker = *ttlCheckTicker 187 | } 188 | 189 | if *dnsTTLCache == 0 && config.DNSTTLCache == 0 { 190 | config.DNSTTLCache = -1 191 | } else if *dnsTTLCache != 0 { 192 | config.DNSTTLCache = *dnsTTLCache 193 | } 194 | 195 | if *whitelistLoopback || config.WhitelistLoEnabled { 196 | config.WhitelistLo = []string{"127.0.0.0/8"} 197 | } 198 | 199 | if *whitelistPrivate || config.WhitelistPrivateEnabled { 200 | config.WhitelistPrivate = []string{ 201 | "10.0.0.0/8", 202 | "172.16.0.0/12", 203 | "192.168.0.0/16", 204 | "100.64.0.0/10", 205 | } 206 | } 207 | 208 | return config, nil 209 | } 210 | 211 | func fileExists(file string) error { 212 | f, err := os.Stat(file) 213 | if os.IsNotExist(err) { 214 | return err 215 | } 216 | 217 | if f.IsDir() { 218 | return fmt.Errorf("[%s] is a directory", file) 219 | } 220 | 221 | return nil 222 | } 223 | -------------------------------------------------------------------------------- /dns/listeners.go: -------------------------------------------------------------------------------- 1 | package dns 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "fmt" 7 | "sync" 8 | "time" 9 | 10 | "github.com/miekg/dns" 11 | "github.com/sirupsen/logrus" 12 | ) 13 | 14 | // ServiceContext for canceling goroutins 15 | type ServiceContext struct { 16 | cancel context.CancelFunc 17 | wg *sync.WaitGroup 18 | } 19 | 20 | // Expire will call cancel to terminate a context immediately, causing the goroutine to exit 21 | func (f *ServiceContext) Expire() { 22 | f.cancel() 23 | } 24 | 25 | // Wait ensures that the goroutine has exit successfully 26 | func (f *ServiceContext) Wait() { 27 | f.wg.Wait() 28 | } 29 | 30 | // dnsTTLCacheManager spawns a goroutine for checking cache for expired queries 31 | func (s *Server) dnsTTLCacheManager() (*ServiceContext, error) { 32 | if s.cache == nil { 33 | return nil, fmt.Errorf(errNil) 34 | } 35 | 36 | s.logger.WithFields(logrus.Fields{ 37 | "Component": "DNS Cache", 38 | "Stage": "Init", 39 | }).Info("Starting DNS TTL Cache Manager") 40 | 41 | dnsCacheContext := &ServiceContext{} 42 | 43 | var serviceWG sync.WaitGroup 44 | dnsCacheContext.wg = &serviceWG 45 | 46 | ctx, cancel := context.WithCancel(context.Background()) 47 | dnsCacheContext.cancel = cancel 48 | 49 | serviceWG.Add(1) 50 | go func(ctx context.Context, wg *sync.WaitGroup, l *logrus.Entry) { 51 | l = l.WithFields(logrus.Fields{ 52 | "Component": "DNS Cache", 53 | "Stage": "Cache Watcher", 54 | }) 55 | ticker := time.NewTicker(30 * time.Second) 56 | for { 57 | select { 58 | case <-ctx.Done(): 59 | l = l.WithFields(logrus.Fields{ 60 | "Component": "DNS Cache", 61 | "Stage": "Term", 62 | }) 63 | 64 | l.Info("Bye!") 65 | wg.Done() 66 | return 67 | case <-ticker.C: 68 | if s.cache.GetTTL() < 0 { 69 | break 70 | } 71 | l.Debug("Checking DNS Cache") 72 | for _, h := range s.cache.ExpiredQueries() { 73 | l.Debugf("Deleting host [%s] from cache", h) 74 | s.cache.Delete(h) 75 | } 76 | l.Debugf("Freeing up DNS Cache memory") 77 | s.cache.NewResolved() 78 | for _, h := range s.cache.ExpiredMXQueries() { 79 | l.Debugf("Deleting host [%s] from NX cache", h) 80 | s.cache.DeleteNX(h) 81 | } 82 | l.Debugf("Freeing up DNS NX Cache memory") 83 | s.cache.NewNX() 84 | default: 85 | time.Sleep(time.Millisecond * 50) 86 | } 87 | } 88 | }(ctx, &serviceWG, s.fwdl) 89 | 90 | return dnsCacheContext, nil 91 | } 92 | 93 | // UDPListenBackground for spawning a udp DNS Server 94 | func (s *Server) UDPListenBackground(fn func(resp *dns.Msg) error) *ServiceContext { 95 | s.logger.WithFields(logrus.Fields{ 96 | "Component": "DNS Server", 97 | "Stage": "Init", 98 | }).Info("Starting UDP DNS Server") 99 | 100 | dnsServerContext := &ServiceContext{} 101 | 102 | var serviceListenerWG sync.WaitGroup 103 | dnsServerContext.wg = &serviceListenerWG 104 | 105 | ctxListener, cancelListener := context.WithCancel(context.Background()) 106 | dnsServerContext.cancel = cancelListener 107 | 108 | s.udpServer = &dns.Server{ 109 | //UDPSize: 4096, 110 | Addr: s.listenAddr, Net: "udp", 111 | Handler: dns.HandlerFunc( 112 | func(w dns.ResponseWriter, r *dns.Msg) { 113 | s.fwd(w, r, fn) 114 | }, 115 | ), 116 | } 117 | 118 | serviceListenerWG.Add(1) 119 | go func(wg *sync.WaitGroup, srv *dns.Server) { 120 | l := s.logger.WithFields(logrus.Fields{ 121 | "Component": "[UDP] DNSServer", 122 | "Stage": "Init", 123 | }) 124 | 125 | l.Info("Starting") 126 | if err := srv.ListenAndServe(); err != nil { 127 | l.Error(err) 128 | } 129 | wg.Done() 130 | }(&serviceListenerWG, s.udpServer) 131 | 132 | serviceListenerWG.Add(1) 133 | go func(ctx, ctxOnErr context.Context, wg *sync.WaitGroup, srv *dns.Server) { 134 | l := s.logger.WithFields(logrus.Fields{ 135 | "Component": "[UDP] DNSServer", 136 | "Stage": "Term", 137 | }) 138 | 139 | for { 140 | select { 141 | case <-ctx.Done(): 142 | if err := srv.Shutdown(); err != nil { 143 | l.Fatal(err) 144 | } 145 | s.cacheContext.Expire() 146 | s.cacheContext.Wait() 147 | l.Info("Bye!") 148 | wg.Done() 149 | return 150 | case <-ctxOnErr.Done(): 151 | s.killOnErr() 152 | default: 153 | time.Sleep(time.Millisecond * 50) 154 | } 155 | } 156 | }(ctxListener, s.ctxOnErr, &serviceListenerWG, s.udpServer) 157 | 158 | return dnsServerContext 159 | } 160 | 161 | // TCPListenBackground for spawning a tcp DNS Server 162 | func (s *Server) TCPListenBackground(fn func(resp *dns.Msg) error) *ServiceContext { 163 | s.logger.WithFields(logrus.Fields{ 164 | "Component": "DNS Server", 165 | "Stage": "Init", 166 | }).Info("Starting TCP DNS Server") 167 | 168 | dnsServerContext := &ServiceContext{} 169 | 170 | var serviceListenerWG sync.WaitGroup 171 | dnsServerContext.wg = &serviceListenerWG 172 | 173 | ctxListener, cancelListener := context.WithCancel(context.Background()) 174 | dnsServerContext.cancel = cancelListener 175 | 176 | s.tcpServer = &dns.Server{ 177 | Addr: s.listenAddr, Net: "tcp", 178 | Handler: dns.HandlerFunc( 179 | func(w dns.ResponseWriter, r *dns.Msg) { 180 | s.fwd(w, r, fn) 181 | }, 182 | ), 183 | } 184 | 185 | serviceListenerWG.Add(1) 186 | go func(wg *sync.WaitGroup, srv *dns.Server) { 187 | l := s.logger.WithFields(logrus.Fields{ 188 | "Component": "[TCP] DNSServer", 189 | "Stage": "Init", 190 | }) 191 | 192 | if s.listenTLS { 193 | l = s.logger.WithFields(logrus.Fields{ 194 | "Component": "[TLS] DNSServer", 195 | "Stage": "Init", 196 | }) 197 | 198 | srv.TLSConfig = &tls.Config{ 199 | Certificates: []tls.Certificate{*s.listenCerts}, 200 | } 201 | 202 | srv.Net = "tcp-tls" 203 | } 204 | 205 | l.Info("Starging") 206 | 207 | if err := srv.ListenAndServe(); err != nil { 208 | l.Error(err) 209 | } 210 | wg.Done() 211 | }(&serviceListenerWG, s.tcpServer) 212 | 213 | serviceListenerWG.Add(1) 214 | go func(ctx, ctxOnErr context.Context, wg *sync.WaitGroup, srv *dns.Server) { 215 | l := s.logger.WithFields(logrus.Fields{ 216 | "Component": "[TCP] DNSServer", 217 | "Stage": "Term", 218 | }) 219 | for { 220 | select { 221 | case <-ctx.Done(): 222 | if err := srv.Shutdown(); err != nil { 223 | l.Fatal(err) 224 | } 225 | s.cacheContext.Expire() 226 | s.cacheContext.Wait() 227 | l.Info("Bye!") 228 | wg.Done() 229 | return 230 | case <-ctxOnErr.Done(): 231 | s.killOnErr() 232 | default: 233 | time.Sleep(time.Millisecond * 50) 234 | } 235 | } 236 | }(ctxListener, s.ctxOnErr, &serviceListenerWG, s.tcpServer) 237 | 238 | return dnsServerContext 239 | } 240 | -------------------------------------------------------------------------------- /cmd/nettrust.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/sirupsen/logrus" 7 | "github.com/ulfox/nettrust/authorizer" 8 | "github.com/ulfox/nettrust/dns" 9 | "github.com/ulfox/nettrust/firewall" 10 | 11 | "github.com/ulfox/nettrust/core" 12 | ) 13 | 14 | const ( 15 | tableNameOutput = "net-trust" 16 | chainNameOutput = "authorized-output" 17 | authorizedSet = "authorized" 18 | chainNameInput = "input" 19 | ) 20 | 21 | var ( 22 | logger *logrus.Logger 23 | ) 24 | 25 | func main() { 26 | 27 | logger = logrus.New() 28 | logger.SetFormatter( 29 | &logrus.TextFormatter{FullTimestamp: true}, 30 | ) 31 | 32 | log := logger.WithFields(logrus.Fields{ 33 | "Component": "NetTrust", 34 | "Stage": "main", 35 | }) 36 | 37 | // Read Nettrust environment and config 38 | config, err := core.GetNetTrustEnv() 39 | if err != nil { 40 | log.Fatal(err) 41 | } 42 | // Does nothing for now. Check ToDo.md, debug logs will be added in the future 43 | if config.Env["debug"] == "true" { 44 | logger.SetLevel(logrus.DebugLevel) 45 | } 46 | 47 | if !config.DoNotFlushTable { 48 | log.Warn(core.WarnOnExitFlush) 49 | } 50 | 51 | if config.DoNotFlushAuthorizedHosts { 52 | log.Warn("on exit NetTrust will not flush the authorized hosts list") 53 | } 54 | 55 | // DNS Server 56 | dnsServer, err := dns.NewDNSServer( 57 | config.ListenAddr, 58 | config.FWDAddr, 59 | config.FWDProto, 60 | config.ListenCert, 61 | config.ListenCertKey, 62 | config.FWDCaCert, 63 | config.ListenTLS, 64 | config.FWDTLS, 65 | config.DNSTTLCache, 66 | config.FWDUDPBufferSize, 67 | config.Blacklist.Domains, 68 | logger, 69 | ) 70 | if err != nil { 71 | log.Fatal(err) 72 | } 73 | 74 | // Firewall 75 | fw, err := firewall.NewFirewall( 76 | config.FirewallBackend, 77 | config.FirewallType, 78 | tableNameOutput, 79 | chainNameOutput, 80 | config.FirewallDropInput, 81 | logger, 82 | ) 83 | if err != nil { 84 | log.Fatal(err) 85 | } 86 | 87 | // Create default chains, tables and rules 88 | // This also applies any whitelist that may have been provided 89 | err = makeDefaultRules(fw, config) 90 | if err != nil { 91 | log.Fatal(err) 92 | } 93 | 94 | for k, v := range config.Env { 95 | if strings.HasPrefix(k, "blacklist.networks") { 96 | err = core.CheckIPV4Network(v) 97 | if err != nil { 98 | log.Fatal(err) 99 | } 100 | config.Blacklist.Networks = append(config.Blacklist.Networks, v) 101 | } 102 | } 103 | 104 | for k, v := range config.Env { 105 | if strings.HasPrefix(k, "blacklist.hosts") { 106 | err = core.CheckIPV4Addresses(v) 107 | if err != nil { 108 | log.Fatal(err) 109 | } 110 | config.Blacklist.Hosts = append(config.Blacklist.Hosts, v) 111 | } 112 | } 113 | 114 | authorizer, cacheContext, err := authorizer.NewAuthorizer( 115 | config.AuthorizedTTL, 116 | config.TTLCheckTicker, 117 | authorizedSet, 118 | config.Blacklist.Hosts, 119 | config.Blacklist.Networks, 120 | config.DoNotFlushAuthorizedHosts, 121 | fw, 122 | logger, 123 | ) 124 | if err != nil { 125 | log.Fatal(err) 126 | } 127 | 128 | // Init DNS Servers 129 | udpDNSServerContext := dnsServer.UDPListenBackground( 130 | authorizer.HandleRequest) 131 | tcpDNSServerContext := dnsServer.TCPListenBackground( 132 | authorizer.HandleRequest) 133 | 134 | sysSigs := core.NewOSSignal() 135 | 136 | sysSigs.Wait() 137 | log.Infof("Interrupted") 138 | 139 | udpDNSServerContext.Expire() 140 | tcpDNSServerContext.Expire() 141 | 142 | udpDNSServerContext.Wait() 143 | tcpDNSServerContext.Wait() 144 | 145 | cacheContext.Expire() 146 | cacheContext.Wait() 147 | 148 | if !config.DoNotFlushTable { 149 | log.Info("flush table is enabled, flushing ...") 150 | err = fw.FlushTable(tableNameOutput) 151 | if err != nil { 152 | log.Fatal(err) 153 | } 154 | 155 | err = fw.DeleteChain(chainNameOutput) 156 | if err != nil { 157 | log.Fatal(err) 158 | } 159 | if config.FirewallDropInput { 160 | err = fw.DeleteChain(chainNameInput) 161 | if err != nil { 162 | log.Fatal(err) 163 | } 164 | } 165 | err = fw.DeleteTable(tableNameOutput) 166 | if err != nil { 167 | log.Fatal(err) 168 | } 169 | } 170 | 171 | } 172 | 173 | func makeDefaultRules(fw *firewall.Firewall, config *core.NetTrust) error { 174 | var err error 175 | 176 | for _, v := range config.WhitelistLo { 177 | err = core.CheckIPV4Network(v) 178 | if err != nil { 179 | return err 180 | } 181 | 182 | err = fw.AddIPv4NetworkRule(v) 183 | if err != nil { 184 | return err 185 | } 186 | } 187 | 188 | for _, v := range config.WhitelistPrivate { 189 | err = core.CheckIPV4Network(v) 190 | if err != nil { 191 | return err 192 | } 193 | 194 | err = fw.AddIPv4NetworkRule(v) 195 | if err != nil { 196 | return err 197 | } 198 | } 199 | 200 | for k, v := range config.Env { 201 | if strings.HasPrefix(k, "whitelist.networks") { 202 | err = core.CheckIPV4Network(v) 203 | if err != nil { 204 | return err 205 | } 206 | 207 | err = fw.AddIPv4NetworkRule(v) 208 | if err != nil { 209 | return err 210 | } 211 | } 212 | } 213 | 214 | for _, v := range config.Whitelist.Networks { 215 | err = core.CheckIPV4Network(v) 216 | if err != nil { 217 | return err 218 | } 219 | 220 | err = fw.AddIPv4NetworkRule(v) 221 | if err != nil { 222 | return err 223 | } 224 | } 225 | 226 | err = fw.AddIPv4Set("whitelist") 227 | if err != nil { 228 | return err 229 | } 230 | 231 | err = fw.AddIPv4SetRule("whitelist") 232 | if err != nil { 233 | return err 234 | } 235 | 236 | for _, n := range []string{config.ListenAddr, config.FWDAddr} { 237 | err = core.CheckIPV4SocketAddress(n) 238 | if err != nil { 239 | return err 240 | } 241 | 242 | err = fw.AddIPv4ToSetRule("whitelist", strings.Split(n, ":")[0]) 243 | if err != nil { 244 | return err 245 | } 246 | } 247 | 248 | for k, v := range config.Env { 249 | if strings.HasPrefix(k, "whitelist.hosts") { 250 | err = core.CheckIPV4Addresses(v) 251 | if err != nil { 252 | return err 253 | } 254 | 255 | err = fw.AddIPv4ToSetRule("whitelist", v) 256 | if err != nil { 257 | return err 258 | } 259 | } 260 | } 261 | 262 | for _, v := range config.Whitelist.Hosts { 263 | err = core.CheckIPV4Addresses(v) 264 | if err != nil { 265 | return err 266 | } 267 | 268 | err = fw.AddIPv4ToSetRule("whitelist", v) 269 | if err != nil { 270 | return err 271 | } 272 | } 273 | 274 | err = fw.AddIPv4Set(authorizedSet) 275 | if err != nil { 276 | return err 277 | } 278 | 279 | err = fw.AddIPv4SetRule(authorizedSet) 280 | if err != nil { 281 | return err 282 | } 283 | 284 | err = fw.AddTailingReject() 285 | if err != nil { 286 | return err 287 | } 288 | 289 | return nil 290 | } 291 | -------------------------------------------------------------------------------- /firewall/nftables/chain4.go: -------------------------------------------------------------------------------- 1 | package nftables 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/google/nftables" 8 | "github.com/google/nftables/expr" 9 | ) 10 | 11 | func (f *FirewallBackend) getChain(c string) (*nftables.Chain, error) { 12 | f.Lock() 13 | chains, err := f.nft.ListChains() 14 | f.Unlock() 15 | 16 | if err != nil { 17 | return nil, err 18 | } 19 | for _, t := range chains { 20 | if c == t.Name { 21 | return t, nil 22 | } 23 | } 24 | 25 | return nil, fmt.Errorf(errNoSuchCahin, c) 26 | } 27 | 28 | func (f *FirewallBackend) getTable(c string) (*nftables.Table, error) { 29 | f.Lock() 30 | tables, err := f.nft.ListTables() 31 | f.Unlock() 32 | 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | for _, t := range tables { 38 | if c == t.Name { 39 | return t, nil 40 | } 41 | } 42 | 43 | return nil, fmt.Errorf(errNoSuchTable, c) 44 | } 45 | 46 | // CreateIPv4Table create an nftables table 47 | func (f *FirewallBackend) CreateIPv4Table(table string) error { 48 | _, err := f.getTable(table) 49 | if err != nil { 50 | if !strings.HasPrefix(err.Error(), "could not find table") { 51 | return err 52 | } 53 | } 54 | 55 | f.Lock() 56 | defer f.Unlock() 57 | 58 | f.nft.AddTable(&nftables.Table{ 59 | Name: table, 60 | Family: nftables.TableFamilyIPv4, 61 | }) 62 | 63 | err = f.nft.Flush() 64 | if err != nil { 65 | return err 66 | } 67 | 68 | return nil 69 | } 70 | 71 | // CreateIPv4Chain create an nftables chain in a specific table 72 | func (f *FirewallBackend) CreateIPv4Chain(table, chain, chainType string, hookType int) error { 73 | nt, err := f.getTable(table) 74 | if err != nil { 75 | if !strings.HasPrefix(err.Error(), "could not find table") { 76 | return err 77 | } 78 | } 79 | 80 | // We should build cases on this in the future should we need to add 81 | // additional chain types 82 | var cT nftables.ChainType 83 | switch chainType { 84 | case string(nftables.ChainTypeFilter): 85 | cT = nftables.ChainTypeFilter 86 | case string(nftables.ChainTypeNAT): 87 | cT = nftables.ChainTypeNAT 88 | case string(nftables.ChainTypeRoute): 89 | cT = nftables.ChainTypeRoute 90 | } 91 | 92 | // same here, we should add additional hook ypes if we need 93 | var hT nftables.ChainHook 94 | switch hookType { 95 | case int(nftables.ChainHookOutput): 96 | hT = nftables.ChainHookOutput 97 | case int(nftables.ChainHookInput): 98 | hT = nftables.ChainHookInput 99 | case int(nftables.ChainHookPrerouting): 100 | hT = nftables.ChainHookPrerouting 101 | case int(nftables.ChainHookForward): 102 | hT = nftables.ChainHookForward 103 | } 104 | 105 | _, err = f.getChain(chain) 106 | if err != nil { 107 | if !strings.HasPrefix(err.Error(), "could not find chain") { 108 | return err 109 | } 110 | } 111 | 112 | f.Lock() 113 | defer f.Unlock() 114 | 115 | // drop by default. 116 | // If somehow the reject tailing rule is skipped, 117 | // this will introduced timeouts for processes 118 | // that request to access an non-authorized ip. 119 | outputPolicy := nftables.ChainPolicyDrop 120 | f.nft.AddChain(&nftables.Chain{ 121 | Name: chain, 122 | Table: nt, 123 | Type: cT, 124 | Hooknum: hT, 125 | Priority: nftables.ChainPriorityFilter, 126 | Policy: &outputPolicy, 127 | }) 128 | 129 | err = f.nft.Flush() 130 | if err != nil { 131 | return err 132 | } 133 | 134 | return nil 135 | } 136 | 137 | func (f *FirewallBackend) createChainInputWithEstablished(table *nftables.Table, chain *nftables.Chain) error { 138 | 139 | f.Lock() 140 | rules, err := f.nft.GetRule(table, chain) 141 | f.Unlock() 142 | 143 | if err != nil { 144 | return err 145 | } 146 | 147 | for _, rule := range rules { 148 | for _, e := range rule.Exprs { 149 | ct, ok := e.(*expr.Ct) 150 | if !ok { 151 | continue 152 | } 153 | if ct.Key == expr.CtKeySTATE { 154 | return nil 155 | } 156 | } 157 | } 158 | 159 | f.Lock() 160 | defer f.Unlock() 161 | 162 | f.nft.AddRule(&nftables.Rule{ 163 | Table: table, 164 | Chain: chain, 165 | Exprs: []expr.Any{ 166 | // [ ct load state => reg 1 ] 167 | &expr.Ct{ 168 | Register: 1, 169 | Key: expr.CtKeySTATE, 170 | }, 171 | // [ bitwise reg 1 = (reg=1 & 0x00000006 ) ^ 0x00000000 ] 172 | &expr.Bitwise{ 173 | SourceRegister: 1, 174 | DestRegister: 1, 175 | Len: 4, 176 | Mask: []byte{0x06, 0x00, 0x00, 0x00}, 177 | Xor: []byte{0x00, 0x00, 0x00, 0x00}, 178 | }, 179 | // [ cmp neq reg 1 0x00000000 ] 180 | &expr.Cmp{ 181 | Op: expr.CmpOpNeq, 182 | Register: 1, 183 | Data: []byte{0x00, 0x00, 0x00, 0x00}, 184 | }, 185 | &expr.Counter{}, 186 | &expr.Verdict{Kind: expr.VerdictAccept}, 187 | }, 188 | }) 189 | 190 | err = f.nft.Flush() 191 | if err != nil { 192 | return err 193 | } 194 | 195 | f.nft.AddRule(&nftables.Rule{ 196 | Table: table, 197 | Chain: chain, 198 | Exprs: []expr.Any{ 199 | // [ meta load iifname => reg 1 ] 200 | &expr.Meta{ 201 | Register: 1, 202 | Key: expr.MetaKeyIIFNAME, 203 | }, 204 | // [ cmp eq reg 1 0x00006f6c 0x00000000 0x00000000 0x00000000 ] 205 | &expr.Cmp{ 206 | Op: expr.CmpOpEq, 207 | Register: 1, 208 | Data: []byte{0x6c, 0x6f, 0x00, 0x00}, 209 | }, 210 | &expr.Verdict{Kind: expr.VerdictAccept}, 211 | }, 212 | }) 213 | 214 | return f.nft.Flush() 215 | } 216 | 217 | // AddTailingReject is responsible for appending a reject verdict at the end of the chain. If reject verdict 218 | // is not a tailing verdict it will move it at the end by first creating a new reject verdict at the end of 219 | // the chain and then deleting the existing one 220 | func (f *FirewallBackend) AddTailingReject() error { 221 | f.Lock() 222 | rules, err := f.nft.GetRule(f.table, f.chain) 223 | f.Unlock() 224 | 225 | if err != nil { 226 | return err 227 | } 228 | 229 | totalRules := len(rules) 230 | for i, r := range rules { 231 | if len(r.Exprs) == 1 { 232 | _, ok := r.Exprs[0].(*expr.Counter) 233 | if !ok { 234 | continue 235 | } 236 | 237 | if i != (totalRules - 1) { 238 | f.Lock() 239 | f.nft.AddRule(&nftables.Rule{ 240 | Table: f.table, 241 | Chain: f.chain, 242 | Exprs: []expr.Any{ 243 | &expr.Counter{}, 244 | &expr.Reject{}, 245 | }, 246 | }) 247 | err = f.nft.Flush() 248 | f.Unlock() 249 | 250 | if err != nil { 251 | return err 252 | } 253 | 254 | f.Lock() 255 | err = f.nft.DelRule(&nftables.Rule{ 256 | Table: f.table, 257 | Chain: f.chain, 258 | Handle: r.Handle, 259 | }) 260 | 261 | if err != nil { 262 | return err 263 | } 264 | 265 | err = f.nft.Flush() 266 | f.Unlock() 267 | 268 | if err != nil { 269 | return err 270 | } 271 | } 272 | 273 | return nil 274 | } 275 | } 276 | 277 | f.Lock() 278 | defer f.Unlock() 279 | 280 | f.nft.AddRule(&nftables.Rule{ 281 | Table: f.table, 282 | Chain: f.chain, 283 | Exprs: []expr.Any{ 284 | &expr.Counter{}, 285 | &expr.Reject{}, 286 | }, 287 | }) 288 | return f.nft.Flush() 289 | } 290 | 291 | // FlushTable Remove rules from chain. This will leave the chain with the defined policy 292 | // If the policy is drop, we should run DeleteChain also if we want the host 293 | // to be able to do network communication 294 | func (f *FirewallBackend) FlushTable(t string) error { 295 | table, err := f.getTable(t) 296 | if err != nil { 297 | return err 298 | } 299 | 300 | f.Lock() 301 | defer f.Unlock() 302 | 303 | f.nft.FlushTable(table) 304 | 305 | return f.nft.Flush() 306 | } 307 | 308 | // DeleteChain Delete chain from the table. By removing the chain we allow all communication 309 | // if no other rules are set by external tools 310 | func (f *FirewallBackend) DeleteChain(c string) error { 311 | chain, err := f.getChain(c) 312 | if err != nil { 313 | return err 314 | } 315 | 316 | f.Lock() 317 | defer f.Unlock() 318 | 319 | f.nft.DelChain(chain) 320 | 321 | return f.nft.Flush() 322 | } 323 | 324 | // DeleteTable Delete Table from nftables 325 | func (f *FirewallBackend) DeleteTable(t string) error { 326 | table, err := f.getTable(t) 327 | if err != nil { 328 | return err 329 | } 330 | 331 | f.Lock() 332 | defer f.Unlock() 333 | 334 | f.nft.DelTable(table) 335 | 336 | return f.nft.Flush() 337 | } 338 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/BurntSushi/toml v0.4.1 h1:GaI7EiDXDRfa8VshkTj7Fym7ha+y8/XxIgD2okUIjLw= 3 | github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 4 | github.com/cilium/ebpf v0.5.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= 5 | github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA= 6 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= 10 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 11 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 12 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 13 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 14 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 15 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 16 | github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= 17 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 18 | github.com/google/nftables v0.0.0-20220210072902-edf9fe8cd04f h1:KpLWaUqH5AiBiitSoiIP+xYor9XPuvVKHyq7bmsPr98= 19 | github.com/google/nftables v0.0.0-20220210072902-edf9fe8cd04f/go.mod h1:0F8on3JWMkm+xahTHItkiu/E1SPqMd0TOxNweQv8ptE= 20 | github.com/josharian/native v0.0.0-20200817173448-b6b71def0850 h1:uhL5Gw7BINiiPAo24A2sxkcDI0Jt/sqp1v5xQCniEFA= 21 | github.com/josharian/native v0.0.0-20200817173448-b6b71def0850/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= 22 | github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw= 23 | github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ= 24 | github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok= 25 | github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391/go.mod h1:cR77jAZG3Y3bsb8hF6fHJbFoyFukLFOkQ98S0pQz3xw= 26 | github.com/jsimonetti/rtnetlink v0.0.0-20201220180245-69540ac93943/go.mod h1:z4c53zj6Eex712ROyh8WI0ihysb5j2ROyV42iNogmAs= 27 | github.com/jsimonetti/rtnetlink v0.0.0-20210122163228-8d122574c736/go.mod h1:ZXpIyOK59ZnN7J0BV99cZUPmsqDRZ3eq5X+st7u/oSA= 28 | github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b/go.mod h1:8w9Rh8m+aHZIG69YPGGem1i5VzoyRC8nw2kA8B+ik5U= 29 | github.com/jsimonetti/rtnetlink v0.0.0-20210525051524-4cc836578190/go.mod h1:NmKSdU4VGSiv1bMsdqNALI4RSvvjtz65tTMCnD05qLo= 30 | github.com/jsimonetti/rtnetlink v0.0.0-20211022192332-93da33804786 h1:N527AHMa793TP5z5GNAn/VLPzlc0ewzWdeP/25gDfgQ= 31 | github.com/jsimonetti/rtnetlink v0.0.0-20211022192332-93da33804786/go.mod h1:v4hqbTdfQngbVSZJVWUhGE/lbTFf9jb+ygmNUDQMuOs= 32 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 33 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 34 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 35 | github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43/go.mod h1:+t7E0lkKfbBsebllff1xdTmyJt8lH37niI6kwFk9OTo= 36 | github.com/mdlayher/ethtool v0.0.0-20211028163843-288d040e9d60 h1:tHdB+hQRHU10CfcK0furo6rSNgZ38JT8uPh70c/pFD8= 37 | github.com/mdlayher/ethtool v0.0.0-20211028163843-288d040e9d60/go.mod h1:aYbhishWc4Ai3I2U4Gaa2n3kHWSwzme6EsG/46HRQbE= 38 | github.com/mdlayher/genetlink v1.0.0 h1:OoHN1OdyEIkScEmRgxLEe2M9U8ClMytqA5niynLtfj0= 39 | github.com/mdlayher/genetlink v1.0.0/go.mod h1:0rJ0h4itni50A86M2kHcgS85ttZazNt7a8H2a2cw0Gc= 40 | github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA= 41 | github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M= 42 | github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY= 43 | github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o= 44 | github.com/mdlayher/netlink v1.1.2-0.20201013204415-ded538f7f4be/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o= 45 | github.com/mdlayher/netlink v1.2.0/go.mod h1:kwVW1io0AZy9A1E2YYgaD4Cj+C+GPkU6klXCMzIJ9p8= 46 | github.com/mdlayher/netlink v1.2.1/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU= 47 | github.com/mdlayher/netlink v1.2.2-0.20210123213345-5cc92139ae3e/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU= 48 | github.com/mdlayher/netlink v1.3.0/go.mod h1:xK/BssKuwcRXHrtN04UBkwQ6dY9VviGGuriDdoPSWys= 49 | github.com/mdlayher/netlink v1.4.0/go.mod h1:dRJi5IABcZpBD2A3D0Mv/AiX8I9uDEu5oGkAVrekmf8= 50 | github.com/mdlayher/netlink v1.4.1/go.mod h1:e4/KuJ+s8UhfUpO9z00/fDZZmhSrs+oxyqAS9cNgn6Q= 51 | github.com/mdlayher/netlink v1.4.2 h1:3sbnJWe/LETovA7yRZIX3f9McVOWV3OySH6iIBxiFfI= 52 | github.com/mdlayher/netlink v1.4.2/go.mod h1:13VaingaArGUTUxFLf/iEovKxXji32JAtF858jZYEug= 53 | github.com/mdlayher/socket v0.0.0-20210307095302-262dc9984e00/go.mod h1:GAFlyu4/XV68LkQKYzKhIo/WW7j3Zi0YRAz/BOoanUc= 54 | github.com/mdlayher/socket v0.0.0-20211007213009-516dcbdf0267/go.mod h1:nFZ1EtZYK8Gi/k6QNu7z7CgO20i/4ExeQswwWuPmG/g= 55 | github.com/mdlayher/socket v0.0.0-20211102153432-57e3fa563ecb h1:2dC7L10LmTqlyMVzFJ00qM25lqESg9Z4u3GuEXN5iHY= 56 | github.com/mdlayher/socket v0.0.0-20211102153432-57e3fa563ecb/go.mod h1:nFZ1EtZYK8Gi/k6QNu7z7CgO20i/4ExeQswwWuPmG/g= 57 | github.com/miekg/dns v1.1.46 h1:uzwpxRtSVxtcIZmz/4Uz6/Rn7G11DvsaslXoy5LxQio= 58 | github.com/miekg/dns v1.1.46/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= 59 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 60 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 61 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 62 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 63 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 64 | github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= 65 | github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 66 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 67 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 68 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 69 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 70 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 71 | github.com/ti-mo/conntrack v0.4.0 h1:6TZXNqhsJmeBl1Pyzg43Y0V1Nx8jyZ4dpOtItCVXE+8= 72 | github.com/ti-mo/conntrack v0.4.0/go.mod h1:L0vkIzG/TECsuVYMMlID9QWmZQLjyP9gDq8XKTlbg4Q= 73 | github.com/ti-mo/netfilter v0.3.1 h1:+ZTmeTx+64Jw2N/1gmqm42kruDWjQ90SMjWEB1e6VDs= 74 | github.com/ti-mo/netfilter v0.3.1/go.mod h1:t/5HvCCHA1LAYj/AZF2fWcJ23BQTA7lzTPCuwwi7xQY= 75 | github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc h1:R83G5ikgLMxrBvLh22JhdfI8K6YXEPHx5P03Uu3DRs4= 76 | github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI= 77 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 78 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 79 | github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 80 | github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 81 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 82 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 83 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 84 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 85 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 86 | golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38= 87 | golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= 88 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 89 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 90 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 91 | golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 92 | golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 93 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 94 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 95 | golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 96 | golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 97 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 98 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 99 | golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 100 | golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 101 | golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 102 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 103 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 104 | golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 105 | golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 106 | golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 107 | golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 108 | golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 109 | golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 110 | golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 111 | golang.org/x/net v0.0.0-20211209124913-491a49abca63 h1:iocB37TsdFuN6IBRZ+ry36wrkoV51/tl5vOWqkcPGvY= 112 | golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 113 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 114 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 115 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= 116 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 117 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 118 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 119 | golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 120 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 121 | golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 122 | golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 123 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 124 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 125 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 126 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 127 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 128 | golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 129 | golang.org/x/sys v0.0.0-20201017003518-b09fb700fbb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 130 | golang.org/x/sys v0.0.0-20201118182958-a01c418693c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 131 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 132 | golang.org/x/sys v0.0.0-20201218084310-7d0127a74742/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 133 | golang.org/x/sys v0.0.0-20210110051926-789bb1bd4061/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 134 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 135 | golang.org/x/sys v0.0.0-20210123111255-9b0068b26619/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 136 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 137 | golang.org/x/sys v0.0.0-20210216163648-f7da38b97c65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 138 | golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 139 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 140 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 141 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 142 | golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 143 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 144 | golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 145 | golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 146 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 147 | golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 148 | golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 149 | golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 150 | golang.org/x/sys v0.0.0-20211205182925-97ca703d548d h1:FjkYO/PPp4Wi0EAUOVLxePm7qVW4r4ctbWpURyuOD0E= 151 | golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 152 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 153 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 154 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 155 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 156 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 157 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 158 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 159 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= 160 | golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 161 | golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= 162 | golang.org/x/tools v0.1.8 h1:P1HhGGuLW4aAclzjtmJdf0mJOjVUZUzOTqkAkWL+l6w= 163 | golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= 164 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 165 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 166 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 167 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 168 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 169 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 170 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 171 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 172 | honnef.co/go/tools v0.2.1/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY= 173 | honnef.co/go/tools v0.2.2 h1:MNh1AVMyVX23VUHE2O27jm6lNj3vjO5DexS4A1xvnzk= 174 | honnef.co/go/tools v0.2.2/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY= 175 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NetTrust: Dynamic Outbound Firewall Authorizer 2 | 3 | NetTrust is a Dynamic Outbound Firewall Authorizer. It uses a DNS as a source of truth to allow/deny outbund requests 4 | 5 | ## Overview 6 | 7 | The ideas is that we want to grant network access only to networks or hosts that we trust. Trusted networks and hosts are whitelisted in Output Netfilter Hook, while all others are rejected. 8 | 9 | To increase security or privacy, we usually want to block outbound traffic to: 10 | 11 | - Blocked DNS Queries 12 | - Direct Network Communication (Static IP, no Query made) 13 | 14 | For the first item in the list, this is known as DNS Black hole and is a secure way to narrow down network communication only to trusted domains. However, not all processes (or javascript functions for example) use DNS Queries. There are many who use static IPs to communicate with the outside world. For example, a javascript function could dynamically fetch a list of hosts during render and forward traffic to them. For such case, DNS Blackholes are worethless. 15 | 16 | Firewalls normally allow outbound access to all hosts but restrict inbound access to a selected few 17 | 18 | ```bash 19 | _________ _____________________ 20 | | | Direct Communication IPv4: x.x.x.x | | 21 | | Process |=--------------------------------------> | OUTBOUND: Allow All | 22 | |_________| |_____________________| 23 | | 24 | | _____________________________ 25 | | | | 26 | |<--------------------------------------------=| INBOUND: Few or Established | 27 | |_____________________________| 28 | 29 | ``` 30 | 31 | The allow all to public but filter inbound works well with servers. There we trust the services that we run and we control components in a more strict way 32 | 33 | But what happens when we want to filter and increase security on hosts that are not as restricted as servers or on hosts that may do many things, like personal computers. We install packages often, visit different websites which exposes us to different kind of tracking (telemetries, etc) and risks (hostile apps, bad javascript functions sending traffic to hosts we can not easilly stop) 34 | 35 | The problem here is is that it is hard to filter all good hosts due to: 36 | 37 | - big number of public IPs. The total number of IPv4 addresses may be small for the world, but is really huge to filter it in a list 38 | - hosts usually change IPs, which makes the management of a whitelist even harder 39 | 40 | A work around this issue (up to a way, because as always everything has its weaknesses), is to use DNS for traffic authorization. 41 | 42 | ### DNS Authorizer 43 | 44 | DNS Authorizers are normal DNS hosts that we trust a lot. For example a local DNS service that we have configured to blacklist certain domains, or block all except some domains 45 | 46 | With DNS Authorizer we can: 47 | 48 | - Use them to filter our unwanted domains (most common, needs a list of bad hosts) 49 | - Use them to filter in only wanted domains (most secure, needs a lot more work ) 50 | 51 | By using a DNS Authorizer we have the pros of a DNS Blackhole + easy filtering of IPs. All we need, is to block all outbound traffic and then allow only the traffic that DNS answers in the queries 52 | 53 | This is how NetTrust works. It is small dns proxy with Netfilter management capabilities. 54 | 55 | ```bash 56 | 57 | ________________ ____________ ____________ 58 | | | Query: example.com | | Forward | | 59 | | Host Process A |=------------------------> | NetTrust |=--------------> | DNS Server | 60 | |________________| |____________| |____________| 61 | 62 | ``` 63 | 64 | In the above diagram queries are sent to NetTrust, and from there NetTrust forwards them to the DNS Server that either knows the question or has been configured to forward queries. 65 | 66 | 67 | ```bash 68 | 69 | ________________ ____________ ____________ 70 | | | Query: example.com | | Reply OK | | 71 | | Host Process A |=------------------------> | NetTrust | <--------------=| DNS Server | 72 | |________________| |____________| |____________| 73 | | | 74 | | | 75 | | | 76 | | | 77 | | | _____________ 78 | | | | | 79 | | |=-----> | Netfilter | 80 | | | | 81 | | ------------- 82 | ______________ | 83 | | | x.x.x.x is whitelisted | 84 | | OUTPUT HOOK | <------------------------------------------------=| 85 | |______________| 86 | ``` 87 | 88 | Once NetTrust receives a query response, it checks if there are any answers (hosts resolved). If there are, it proceeds by updating firewall rules (e.g. nftables) in order to allow network access to the resolved hosts. If there is no answer, or if the answer is **0.0.0.0**, no action is taken. In all cases, the dns reply is sent back to the requestor process after a firewall decision has been made (if any). 89 | 90 | ### Authorized hosts TTL 91 | 92 | NetTrust by default does not enable TTL on authorized hosts. The max authorized time a host can get is the time that NetTrust runs. Once NetTrust exits gracefully, it will clear the authorized hosts. 93 | 94 | We can enable however TTL on authorized hosts. By adding a TTL, NetTrust will allow communication to that host for as long as TTL is set. Once a host is expired and no session is active (see Conntrack section below), it will be removed from the authorized list and will be expected by the process that wants to continue communication to resolve the host via the DNS again. 95 | 96 | #### Conntrack: Session liveness and TTL 97 | 98 | All sessions that have TTL enabled will be checked against two rules. The first rule is the TTL itself. If the host has not expired, nothing happens, if it has expired, then conntrack will be checked to ensure that no connection with the specific host is active. If a tuple contains the host, either in the src or dst, then the TTL will be renewed and the host will be checked again in the next expiration. If the host is not part of any conntrack connection, then the host will be removed from the cache and the firewall's authorized hosts set 99 | 100 | ```bash 101 | _______ _____________ 102 | | | Get Expired Hosts | | 103 | | Cache |=------------------------------> | TTL Checker | 104 | |_______| |_____________| 105 | | 106 | | 107 | __________|__________ 108 | | | 109 | | Has host X Expired? | 110 | |_____________________| 111 | | 112 | | Yes 113 | _______________ __________|___________ ___________ 114 | | | No | | Yes | | 115 | | De-Authorize | <------------------=| Is connection active |=--------------> | Renew TTL | 116 | |_______________| |______________________| |___________| 117 | | 118 | | 119 | | 120 | ___________ | 121 | | | Get Active Connections | 122 | | Conntrack |=---------------------------------> | 123 | |___________| 124 | ``` 125 | 126 | ## Build 127 | 128 | To build NetTrust, simply issue: 129 | 130 | ```bash 131 | go build -o nettrust cmd/nettrust.go 132 | ``` 133 | 134 | ## Run NetTrust 135 | 136 | Note: NetTrust needs to interact Netfilter, for that, it requires root access 137 | 138 | To run NetTrust, issue `./nettrust -fwd-addr "someIP:53" -listen-addr "127.0.0.1:53 -config config.json"` 139 | 140 | - listen-addr is the listening address that NetTrust will listen and forward dns queries 141 | - fwd-addr is the address of the DNS Server that NetTrust will use to resolve queries 142 | 143 | Example output 144 | 145 | ```bash 146 | INFO[2022-02-12T20:40:16+02:00] Starting UDP DNS Server Component="DNS Server" Stage=Init 147 | INFO[2022-02-12T20:40:16+02:00] Starting TCP DNS Server Component="DNS Server" Stage=Init 148 | INFO[2022-02-12T20:40:16+02:00] Starting Component="[UDP] DNSServer" Stage=Init 149 | INFO[2022-02-12T20:40:16+02:00] Starging Component="[TCP] DNSServer" Stage=Init 150 | # Some time later 151 | INFO[2022-02-12T20:41:02+02:00] [Blocked] Question: example.com. Component=Firewall Stage=Authorizer 152 | INFO[2022-02-12T20:41:02+02:00] [Not Handled] Question: example.com.federation.local. - Is this local? Component=Firewall Stage=Authorizer 153 | INFO[2022-02-12T20:41:14+02:00] [PTR] Question: 247.1.168.192.in-addr.arpa. resolved to arph.federation.local. Component=Firewall Stage=Authorizer 154 | INFO[2022-02-12T20:41:36+02:00] [Blocked] Question: api.removedButWasSomeDomainHere. Component=Firewall Stage=Authorizer 155 | INFO[2022-02-12T20:41:37+02:00] [Blocked] Question: api.removedButWasSomeDomainHere. Component=Firewall Stage=Authorizer 156 | INFO[2022-02-12T20:41:38+02:00] [Blocked] Question: api.removedButWasSomeDomainHere. Component=Firewall Stage=Authorizer 157 | INFO[2022-02-12T20:41:41+02:00] [Blocked] Question: api.removedButWasSomeDomainHere. Component=Firewall Stage=Authorizer 158 | INFO[2022-02-12T20:41:49+02:00] [Blocked] Question: api.removedButWasSomeDomainHere. Component=Firewall Stage=Authorizer 159 | # Some time later when I did a git push 160 | INFO[2022-02-12T21:19:30+02:00] [Authorized] Question: github.com. Hosts: [140.82.121.4] Component=Firewall Stage=Authorizer 161 | # Some time later when I did a git fetch 162 | INFO[2022-02-12T21:41:54+02:00] [Already Authorized] Question: github.com. Host: 140.82.121.3 Component=Firewall Stage=Authorizer 163 | ``` 164 | 165 | The nftables authorized hosts set now looks like this 166 | 167 | ```bash 168 | table ip net-trust { 169 | set whitelist { 170 | type ipv4_addr 171 | elements = { 127.0.0.1, 192.168.178.21 } 172 | } 173 | 174 | set authorized { 175 | type ipv4_addr 176 | elements = { xyz.xyz.xyz.xyz, xyz.xyz.xyz.xyz, 177 | xyz.xyz.xyz.xyz, xyz.xyz.xyz.xyz, 178 | xyz.xyz.xyz.xyz, xyz.xyz.xyz.xyz, 179 | xyz.xyz.xyz.xyz, xyz.xyz.xyz.xyz, 180 | xyz.xyz.xyz.xyz, xyz.xyz.xyz.xyz, 181 | xyz.xyz.xyz.xyz, xyz.xyz.xyz.xyz, 182 | xyz.xyz.xyz.xyz, 140.82.121.3 } 183 | } 184 | 185 | chain authorized-output { 186 | type filter hook output priority filter; policy drop; 187 | ip daddr 127.0.0.0/8 counter packets 563 bytes 48587 accept 188 | ip daddr 10.0.0.0/8 counter packets 0 bytes 0 accept 189 | ip daddr 172.16.0.0/12 counter packets 0 bytes 0 accept 190 | ip daddr 192.168.0.0/16 counter packets 273 bytes 20402 accept 191 | ip daddr 100.64.0.0/10 counter packets 0 bytes 0 accept 192 | ip daddr @whitelist accept 193 | ip daddr @authorized accept 194 | counter packets 23 bytes 2637 reject with icmp type net-unreachable 195 | } 196 | } 197 | ``` 198 | 199 | Check options below for additional configuration 200 | 201 | ### NetTrust options 202 | 203 | NetTrust accepts the follwoing options 204 | 205 | ```bash 206 | Usage of ./bin/nettrust: 207 | -authorized-ttl int 208 | Number of seconds a authorized host will be active before NetTrust expires it and expect a DNS query again (-1 do not expire) 209 | -config string 210 | Path to config.json 211 | -dns-ttl-cache int 212 | Number of seconds dns queries stay in cache (-1 to disable caching) 213 | -do-not-flush-authorized-hosts 214 | Do not clean up the authorized hosts list on exit. Use this together with do-not-flush-table to keep the NetTrust table as is on exit 215 | -do-not-flush-table 216 | Do not clean up tables when NetTrust exists. Use this flag if you want to continue to deny communication when NetTrust has exited 217 | -firewall-backend string 218 | NetTrust firewall backend [nftables/iptables/iptables-nft] that will be used to interact with Netfilter (nftables is only supported for now) 219 | -firewall-drop-input 220 | If enabled, NetTrust will drop input. Adds [ct state established,related accept] & ['lo' accept]. Should be enabled only when NetTrust runs in host 221 | -firewall-type string 222 | NetTrust firewall type. Supported types: OUTPUT (default), FORWARD. The type essentially tells NetTrust on which hook the rules will be added 223 | -fwd-addr string 224 | NetTrust forward dns address 225 | -fwd-proto string 226 | NetTrust dns forward protocol 227 | -fwd-tls 228 | Enable DoT. This expects that forward dns address supports DoT and fwd-proto is tcp 229 | -fwd-tls-cert string 230 | path to certificate that will be used to validate forward dns hostname. If you do not set this, the the host root CAs will be used 231 | -listen-addr string 232 | NetTrust listen dns address 233 | -listen-cert string 234 | path to certificate that will be used by the TCP DNS Service to serve DoT 235 | -listen-cert-key string 236 | path to the private key that will be used by the TCP DNS Service to serve DoT 237 | -listen-tls 238 | Enable tls listener, tls listener works only with the TCP DNS Service, UDP will continue to serve in plaintext mode 239 | -ttl-check-ticker int 240 | How often NetTrust should check the cache for expired authorized hosts (Checking is blocking, do not put small numbers) 241 | -whitelist-loopback 242 | Loopback network space 127.0.0.0/8 will be whitelisted (default true) 243 | -whitelist-private 244 | If 10.0.0.0/8, 172.16.0.0/16, 192.168.0.0/16, 100.64.0.0/10 will be whitelisted (default true) 245 | ``` 246 | 247 | #### Config options 248 | 249 | You can also use a json config to set options. 250 | 251 | ```json 252 | { 253 | "whitelist": { 254 | "networks": [], 255 | "hosts": [] 256 | }, 257 | "blacklist": { 258 | "networks": [], 259 | "hosts": [] 260 | }, 261 | "fwdAddr": "192.168.178.21:53", // Example address of local dns server 262 | "fwdProto": "udp", 263 | "fwdCaCert": "", 264 | "fwdTLS": false, 265 | 266 | "listenAddr": "127.0.0.1:53", 267 | "listenTLS": false, 268 | "listenCert": "", 269 | "listenCertKey": "", 270 | 271 | "firewallBackend": "nftables", 272 | "firewallType": "OUTPUT", 273 | "firewallDropInput": false, 274 | 275 | "dnsTTLCache": -1, 276 | 277 | "whitelistLoEnabled": true, 278 | "whitelistPrivateEnabled": true, 279 | "ttl": -1, 280 | "ttlInterval": 30, 281 | "doNotFlushTable": false, // Set this to true if you want to keep the rules and the chain when NetTrust has stopped 282 | "doNotFlushAuthorizedHosts": false 283 | } 284 | ``` 285 | 286 | **Note**: Config file options have lower priority from flag options. For example, if you start NetTrust with `-fwd-tls` and you set `fwdTLS: false` in the config, NetTrust will use tls since flags have the highest priority 287 | 288 | ##### Do Not Flush Table on Exit 289 | 290 | **Note**: As you can imagine, with this option set to true, you will not be able to access any host that is not part of a whitelisted option (hosts, networks). If you enabled this option and you wish to revert back, simply start NetTrust again with this option set to false and then exit 291 | 292 | If you wish to keep the table's content on NetTrust exit, then pass either `-do-not-flush-table` via flags or `doNotFlushTable: true` via config. NetTrust on exit will clear only the authorized set, that is the set that is populated via resolved hosts. 293 | 294 | It will keep: 295 | 296 | - The chain with the default policy to drop 297 | - The whitelisted hosts 298 | - The whitelisted networks 299 | - Final reject verdict 300 | 301 | ### NetTrust ENV/Config whitelist / blacklist 302 | 303 | Note: Whitelisting, blacklisting should be done automatically via DNS proxy. This option should be used if you want to add custom entries 304 | 305 | We can add hosts or networks to whitelist or blacklist by using 306 | 307 | - Environmental variables 308 | - config file `config.json` 309 | 310 | Note: Networks are evaluated first in the chain managed by NetTrust (for additional info, see NFTables Overview section at the end of this Readme) 311 | 312 | #### Using Environmental variables 313 | 314 | Whitelist a host by exporting `NET_TRUST_WHITELIST_HOSTS_=someIP` 315 | 316 | ```bash 317 | export NET_TRUST_WHITELIST_HOSTS_CUSTOM=192.168.1.1 318 | ``` 319 | 320 | Whitelist a network by exporting `NET_TRUST_WHITELIST_NETWORK_=someNetwork` 321 | 322 | ```bash 323 | export NET_TRUST_WHITELIST_NETWORK_HOME=192.168.1.0/24 324 | ``` 325 | 326 | Note: blacklisting via env is not yet supported. Check config section in the next section to add blacklists 327 | 328 | #### Using Config file 329 | 330 | Use `config.json` to whitelist or blacklist hosts and networks 331 | 332 | ```bash 333 | { 334 | "whitelist": { 335 | "networks": [], 336 | "hosts": [] 337 | }, 338 | "blacklist": { 339 | "networks": [], 340 | "hosts": [] 341 | } 342 | } 343 | ``` 344 | 345 | Blacklisting instructs NetTrust to skip hosts that match the hostlist or are part of the network. Skipping is essentially blackist since chain's tailing policy is reject and chain's default policy is drop 346 | 347 | 348 | ### NFTables chain overview 349 | 350 | NetTrust creates a table called `net-trust` and a chain called `authorized`. Inside the chain it also creates two sets 351 | 352 | - whitelist: populated by whitelisted hosts during init of NetTrust. This set should stay static during the lifetime of NetTrust (or unless new hosts are whitelisted) 353 | - authorized: this set is used to add authorized hosts. If TTL is set to `-1` then this set should only grow during the lifetime of NetTrust and emptied on exit (we empty to ensure we do not forget whitelisted hosts behind) 354 | 355 | Example of a populated table and chain. Here 127.0.0.1 and 192.168.178.21 are redundant since we whitelisted the networks that contain them, but were added because 127.0.0.1 was the listening address of NetTrust dns proxy and 192.168.178.21 is the IP of a local DNS Black hole. We always whitelist listening address and forward address to ensure that NetTrust will work without issues for cases where NetTrust is started with `-whitelist-private=false` 356 | 357 | ```bash 358 | table ip net-trust { 359 | set whitelist { 360 | type ipv4_addr 361 | elements = { 127.0.0.1, 192.168.178.21 } 362 | } 363 | 364 | set authorized { 365 | type ipv4_addr 366 | } 367 | 368 | chain authorized-output { 369 | type filter hook output priority filter; policy drop; 370 | ip daddr 127.0.0.0/8 counter packets 2389 bytes 469802 accept 371 | ip daddr 10.0.0.0/8 counter packets 0 bytes 0 accept 372 | ip daddr 172.16.0.0/12 counter packets 0 bytes 0 accept 373 | ip daddr 192.168.0.0/16 counter packets 807 bytes 66255 accept 374 | ip daddr 100.64.0.0/10 counter packets 0 bytes 0 accept 375 | ip daddr @whitelist accept 376 | ip daddr @authorized accept 377 | counter packets 14 bytes 1370 reject with icmp type net-unreachable 378 | } 379 | } 380 | ``` 381 | 382 | As you may have noticed, there is no blacklist entry in the chain or in any set. This is because NetTrust uses deny all except firewall implementation. Blacklists are all hosts that are not resolved by the DNS Authority and the hosts added manually via the config file or env vars. The blacklisting is taking place in the DNS Proxy handler, there we check any returned results by the DNS Authority and skip them if they match a blacklist rule 383 | 384 | #### NFTables clean ruleset manually 385 | 386 | If you need to remove NetTrust rules and chains manually, then please follow this section 387 | 388 | ##### Remove rule 389 | 390 | To remove a rule, first get the rule's handle number 391 | 392 | ```bash 393 | sudo nft list table net-trust -a 394 | ``` 395 | 396 | ```bash 397 | table ip net-trust { # handle 5 398 | set whitelist { # handle 7 399 | type ipv4_addr 400 | elements = { 127.0.0.1, 192.168.178.21 } 401 | } 402 | 403 | set authorized { # handle 9 404 | type ipv4_addr 405 | elements = { 140.82.121.4 } 406 | } 407 | 408 | chain authorized-output { # handle 129 409 | type filter hook output priority filter; policy drop; 410 | ip daddr 127.0.0.0/8 counter packets 2174 bytes 196333 accept # handle 130 411 | ip daddr 10.0.0.0/8 counter packets 0 bytes 0 accept # handle 131 412 | ip daddr 172.16.0.0/12 counter packets 0 bytes 0 accept # handle 132 413 | ip daddr 192.168.0.0/16 counter packets 105 bytes 8793 accept # handle 133 414 | ip daddr 100.64.0.0/10 counter packets 0 bytes 0 accept # handle 134 415 | ip daddr @whitelist accept # handle 135 416 | ip daddr @authorized accept # handle 136 417 | counter packets 90 bytes 8380 reject with icmp type net-unreachable # handle 137 418 | } 419 | } 420 | ``` 421 | 422 | To remove rule `ip daddr 100.64.0.0/10 counter packets 0 bytes 0 accept # handle 134` as an example, issue 423 | 424 | ```bash 425 | sudo nft delete rule net-trust authorized-output handle 134 426 | ``` 427 | 428 | ##### Remove all rules from all chains in the table 429 | 430 | To remove all rules from all chains, issue 431 | 432 | ```bash 433 | sudo nft 'flush table net-trust' 434 | ``` 435 | --------------------------------------------------------------------------------