Skip to content

Commit

Permalink
Enable resolving hosts file (#362)
Browse files Browse the repository at this point in the history
fixes #362 

Co-authored-by: Dimitri Herzog <dimitri.herzog@gmail.com>
  • Loading branch information
FileGo and 0xERR0R authored Jan 4, 2022
1 parent 9259c5c commit b43c7aa
Show file tree
Hide file tree
Showing 9 changed files with 481 additions and 5 deletions.
7 changes: 7 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ type Config struct {
CertFile string `yaml:"certFile"`
KeyFile string `yaml:"keyFile"`
BootstrapDNS Upstream `yaml:"bootstrapDns"`
HostsFile HostsFileConfig `yaml:"hostsFile"`
// Deprecated
HTTPCertFile string `yaml:"httpsCertFile"`
// Deprecated
Expand Down Expand Up @@ -386,6 +387,12 @@ type RedisConfig struct {
ConnectionCooldown Duration `yaml:"connectionCooldown" default:"1s"`
}

type HostsFileConfig struct {
Filepath string `yaml:"filePath"`
HostsTTL Duration `yaml:"hostsTTL" default:"1h"`
RefreshPeriod Duration `yaml:"refreshPeriod" default:"1h"`
}

// nolint:gochecknoglobals
var config = &Config{}

Expand Down
8 changes: 8 additions & 0 deletions docs/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,14 @@ httpPort: 4000
bootstrapDns: tcp:1.1.1.1
# optional: Drop all AAAA query if set to true. Default: false
disableIPv6: false
# optional: if path defined, use this file for query resolution (A, AAAA and rDNS). Default: empty
hostsFile:
# optional: Path to hosts file (e.g. /etc/hosts on Linux)
filePath: /etc/hosts
# optional: TTL, default: 1h
hostsTTL: 60m
# optional: Time between hosts file refresh, default: 1h
refreshPeriod: 30m
# optional: Log level (one from debug, info, warn, error). Default: info
logLevel: info
# optional: Log format (text or json). Default: text
Expand Down
28 changes: 24 additions & 4 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ configuration properties as [JSON](config.yml).
| httpPort | [IP]:port[,[IP]:port]* | no | | Port(s) and optional bind ip address(es) to serve HTTP used for prometheus metrics, pprof, REST API, DoH... If you wish to specify a specific IP, you can do so such as `192.168.0.1:4000`. Example: `4000`, `:4000`, `127.0.0.1:4000,[::1]:4000` |
| httpsPort | [IP]:port[,[IP]:port]* | no | | Port(s) and optional bind ip address(es) to serve HTTPS used for prometheus metrics, pprof, REST API, DoH... If you wish to specify a specific IP, you can do so such as `192.168.0.1:443`. Example: `443`, `:443`, `127.0.0.1:443,[::1]:443` |
| certFile | path | yes, if httpsPort > 0 | | Path to cert and key file for SSL encryption (DoH and DoT) |
| keyFile | path | yes, if httpsPort > 0 | | Path to cert and key file for SSL encryption (DoH and DoT) |
| keyFile | path | yes, if httpsPort > 0 | | Path to cert and key file for SSL encryption (DoH and DoT)
| bootstrapDns | IP:port | no | | Use this DNS server to resolve blacklist urls and upstream DNS servers. Useful if no DNS resolver is configured and blocky needs to resolve a host name. NOTE: Works only on Linux/*Nix OS due to golang limitations under windows. |
| disableIPv6 | bool | no | false | Drop all AAAA query if set to true |
| logLevel | enum (debug, info, warn, error) | no | info | Log level |
Expand Down Expand Up @@ -501,17 +501,37 @@ example for CSV format
type: csv
target: /logs
logRetentionDays: 7
```
```

example for Database
!!! example

```yaml
```yaml
queryLog:
type: mysql
target: db_user:db_password@tcp(db_host_or_ip:3306)/db_user?charset=utf8mb4&parseTime=True&loc=Local
logRetentionDays: 7
```
```

### Hosts file

You can enable resolving of entries, located in local hosts file.

Configuration parameters:

| Parameter | Type | Mandatory | Default value | Description |
|--------------------------|--------------------------------|-----------|---------------|-----------------------------------------------|
| hostsFile.filePath | string | no | | Path to hosts file (e.g. /etc/hosts on Linux) |
| hostsFile.hostsTTL | duration (no units is minutes) | no | 1h | TTL |
| hostsFile.refreshPeriod | duration format | no | 1h | Time between hosts file refresh |

!!! example
```yaml
hostsFile:
filePath: /etc/hosts
hostsTTL: 60m
refreshPeriod: 30m
```

## SSL certificate configuration (DoH / TLS listener)

Expand Down
1 change: 1 addition & 0 deletions model/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
// BLOCKED // the query was blocked
// CONDITIONAL // the query was resolved by the conditional upstream resolver
// CUSTOMDNS // the query was resolved by a custom rule
// HOSTSFILE // the query was resolved by looking up the hosts file
// )
type ResponseType int

Expand Down
8 changes: 7 additions & 1 deletion model/models_enum.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

225 changes: 225 additions & 0 deletions resolver/hosts_file_resolver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
package resolver

import (
"fmt"
"net"
"os"
"strings"
"time"

"github.com/0xERR0R/blocky/config"
"github.com/0xERR0R/blocky/model"
"github.com/0xERR0R/blocky/util"
"github.com/miekg/dns"
"github.com/sirupsen/logrus"
)

const (
hostsFileResolverLogger = "hosts_file_resolver"
)

type HostsFileResolver struct {
NextResolver
HostsFilePath string
hosts []host
ttl uint32
refreshPeriod time.Duration
}

func (r *HostsFileResolver) handleReverseDNS(request *model.Request) *model.Response {
question := request.Req.Question[0]
if question.Qtype == dns.TypePTR {
response := new(dns.Msg)
response.SetReply(request.Req)

for _, host := range r.hosts {
raddr, _ := dns.ReverseAddr(host.IP.String())

if raddr == question.Name {
ptr := new(dns.PTR)
ptr.Ptr = dns.Fqdn(host.Hostname)
ptr.Hdr = util.CreateHeader(question, r.ttl)
response.Answer = append(response.Answer, ptr)

for _, alias := range host.Aliases {
ptrAlias := new(dns.PTR)
ptrAlias.Ptr = dns.Fqdn(alias)
ptrAlias.Hdr = util.CreateHeader(question, r.ttl)
response.Answer = append(response.Answer, ptrAlias)
}

return &model.Response{Res: response, RType: model.ResponseTypeHOSTSFILE, Reason: "HOSTS FILE"}
}
}
}

return nil
}

func (r *HostsFileResolver) Resolve(request *model.Request) (*model.Response, error) {
logger := withPrefix(request.Log, hostsFileResolverLogger)

if r.HostsFilePath == "" {
return r.next.Resolve(request)
}

reverseResp := r.handleReverseDNS(request)
if reverseResp != nil {
return reverseResp, nil
}

if len(r.hosts) != 0 {
response := new(dns.Msg)
response.SetReply(request.Req)

question := request.Req.Question[0]
domain := util.ExtractDomain(question)

for _, host := range r.hosts {
if host.Hostname == domain {
if isSupportedType(host.IP, question) {
rr, _ := util.CreateAnswerFromQuestion(question, host.IP, r.ttl)
response.Answer = append(response.Answer, rr)
}
}

for _, alias := range host.Aliases {
if alias == domain {
if isSupportedType(host.IP, question) {
rr, _ := util.CreateAnswerFromQuestion(question, host.IP, r.ttl)
response.Answer = append(response.Answer, rr)
}
}
}
}

if len(response.Answer) > 0 {
logger.WithFields(logrus.Fields{
"answer": util.AnswerToString(response.Answer),
"domain": domain,
}).Debugf("returning hosts file entry")

return &model.Response{Res: response, RType: model.ResponseTypeHOSTSFILE, Reason: "HOSTS FILE"}, nil
}
}

logger.WithField("resolver", Name(r.next)).Trace("go to next resolver")

return r.next.Resolve(request)
}

func (r *HostsFileResolver) Configuration() (result []string) {
if r.HostsFilePath != "" && len(r.hosts) != 0 {
result = append(result, fmt.Sprintf("hosts file path: %s", r.HostsFilePath))
result = append(result, fmt.Sprintf("hosts TTL: %d", r.ttl))
result = append(result, fmt.Sprintf("hosts refresh period: %s", r.refreshPeriod.String()))
} else {
result = []string{"deactivated"}
}

return
}

func NewHostsFileResolver(cfg config.HostsFileConfig) ChainedResolver {
r := HostsFileResolver{
HostsFilePath: cfg.Filepath,
ttl: uint32(time.Duration(cfg.HostsTTL).Seconds()),
refreshPeriod: time.Duration(cfg.RefreshPeriod),
}

err := r.parseHostsFile()

if err != nil {
logger := logger(hostsFileResolverLogger)
logger.Warnf("cannot parse hosts file: %s, hosts file resolving is disabled", r.HostsFilePath)
r.HostsFilePath = ""
} else {
go r.periodicUpdate()
}

return &r
}

type host struct {
IP net.IP
Hostname string
Aliases []string
}

func (r *HostsFileResolver) parseHostsFile() error {
if r.HostsFilePath == "" {
return nil
}

buf, err := os.ReadFile(r.HostsFilePath)
if err != nil {
return err
}

newHosts := make([]host, 0)

for _, line := range strings.Split(string(buf), "\n") {
trimmed := strings.TrimSpace(line)

if len(trimmed) == 0 || trimmed[0] == '#' {
// Skip empty and commented lines
continue
}

// Find comment symbol at the end of the line
var fields []string

end := strings.IndexRune(trimmed, '#')

if end == -1 {
fields = strings.Fields(trimmed)
} else {
fields = strings.Fields(trimmed[:end])
}

if len(fields) < 2 {
// Skip invalid entry
continue
}

if net.ParseIP(fields[0]) == nil {
// Skip invalid IP address
continue
}

var h host
h.IP = net.ParseIP(fields[0])
h.Hostname = fields[1]

if len(fields) > 2 {
for i := 2; i < len(fields); i++ {
h.Aliases = append(h.Aliases, fields[i])
}
}

newHosts = append(newHosts, h)
}

r.hosts = newHosts

return nil
}

func (r *HostsFileResolver) periodicUpdate() {
if r.refreshPeriod > 0 {
ticker := time.NewTicker(r.refreshPeriod)
defer ticker.Stop()

for {
<-ticker.C

logger := logger(hostsFileResolverLogger)
logger.WithField("file", r.HostsFilePath).Debug("refreshing hosts file")

err := r.parseHostsFile()
if err != nil {
logger.Warn("can't refresh hosts file: ", err)
}
}
}
}
Loading

0 comments on commit b43c7aa

Please sign in to comment.