mirror of
https://github.com/coredns/coredns.git
synced 2026-10-08 19:45:21 -04:00
#8496 fixed a silent truncation here and named the invariant in its own commit message: entries were dropped and the hosts file simply looked shorter than it is, with nothing in the log. #8516 replaced that mechanism with a streaming parser bounded by maxFieldSize. The error log #8496 added is still in parse(), but it can no longer report a dropped entry: bufio.ErrBufferFull is consumed by the read loop, so only a real I/O error reaches it. A field over maxFieldSize is discarded in lineParser with no log at all, and when that field is the address the whole line goes with it. Report both cases, once per dropped field, with the line number and the source the entries came from. Signed-off-by: Baltasar Blanco <baltasarblanco.dev@gmail.com>
422 lines
10 KiB
Go
422 lines
10 KiB
Go
// Copyright 2009 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
// This file is a modified version of net/hosts.go from the golang repo
|
|
|
|
package hosts
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"io"
|
|
"net"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/coredns/coredns/plugin"
|
|
)
|
|
|
|
// parseIP calls discards any v6 zone info, before calling net.ParseIP.
|
|
func parseIP(addr string) net.IP {
|
|
if i := strings.Index(addr, "%"); i >= 0 {
|
|
// discard ipv6 zone
|
|
addr = addr[0:i]
|
|
}
|
|
|
|
return net.ParseIP(addr)
|
|
}
|
|
|
|
type options struct {
|
|
// automatically generate IP to Hostname PTR entries
|
|
// for host entries we parse
|
|
autoReverse bool
|
|
|
|
// The TTL of the record we generate
|
|
ttl uint32
|
|
|
|
// The time between two reload of the configuration
|
|
reload time.Duration
|
|
}
|
|
|
|
func newOptions() *options {
|
|
return &options{
|
|
autoReverse: true,
|
|
ttl: 3600,
|
|
reload: 5 * time.Second,
|
|
}
|
|
}
|
|
|
|
// Map contains the IPv4/IPv6 and reverse mapping.
|
|
type Map struct {
|
|
// Key for the list of literal IP addresses must be a FQDN lowercased host name.
|
|
name4 map[string][]net.IP
|
|
name6 map[string][]net.IP
|
|
|
|
// Wildcard owner names (e.g. *.example.com.) map to IP addresses.
|
|
wildName4 map[string][]net.IP
|
|
wildName6 map[string][]net.IP
|
|
|
|
// Key for the list of host names must be a literal IP address
|
|
// including IPv6 address without zone identifier.
|
|
// We don't support old-classful IP address notation.
|
|
addr map[string][]string
|
|
}
|
|
|
|
func newMap() *Map {
|
|
return &Map{
|
|
name4: make(map[string][]net.IP),
|
|
name6: make(map[string][]net.IP),
|
|
wildName4: make(map[string][]net.IP),
|
|
wildName6: make(map[string][]net.IP),
|
|
addr: make(map[string][]string),
|
|
}
|
|
}
|
|
|
|
// Len returns the total number of addresses in the hostmap, this includes V4/V6 and any reverse addresses.
|
|
func (h *Map) Len() int {
|
|
l := 0
|
|
for _, v4 := range h.name4 {
|
|
l += len(v4)
|
|
}
|
|
for _, v6 := range h.name6 {
|
|
l += len(v6)
|
|
}
|
|
for _, v4 := range h.wildName4 {
|
|
l += len(v4)
|
|
}
|
|
for _, v6 := range h.wildName6 {
|
|
l += len(v6)
|
|
}
|
|
for _, a := range h.addr {
|
|
l += len(a)
|
|
}
|
|
return l
|
|
}
|
|
|
|
// Hostsfile contains known host entries.
|
|
type Hostsfile struct {
|
|
sync.RWMutex
|
|
|
|
// list of zones we are authoritative for
|
|
Origins []string
|
|
|
|
// hosts maps for lookups
|
|
hmap *Map
|
|
|
|
// inline saves the hosts file that is inlined in a Corefile.
|
|
inline *Map
|
|
|
|
// path to the hosts file
|
|
path string
|
|
|
|
// mtime and size are only read and modified by a single goroutine
|
|
mtime time.Time
|
|
size int64
|
|
|
|
options *options
|
|
}
|
|
|
|
// readHosts determines if the cached data needs to be updated based on the size and modification time of the hostsfile.
|
|
func (h *Hostsfile) readHosts() {
|
|
file, err := os.Open(h.path)
|
|
if err != nil {
|
|
// We already log a warning if the file doesn't exist or can't be opened on setup. No need to return the error here.
|
|
return
|
|
}
|
|
defer file.Close()
|
|
|
|
stat, err := file.Stat()
|
|
if err != nil {
|
|
return
|
|
}
|
|
h.RLock()
|
|
size := h.size
|
|
mtime := h.mtime
|
|
h.RUnlock()
|
|
|
|
if mtime.Equal(stat.ModTime()) && size == stat.Size() {
|
|
return
|
|
}
|
|
|
|
newMap := h.parse(file)
|
|
log.Debugf("Parsed hosts file into %d entries", newMap.Len())
|
|
|
|
h.Lock()
|
|
|
|
h.hmap = newMap
|
|
// Update the data cache.
|
|
h.mtime = stat.ModTime()
|
|
h.size = stat.Size()
|
|
|
|
hostsEntries.WithLabelValues(h.path).Set(float64(h.inline.Len() + h.hmap.Len()))
|
|
hostsReloadTime.Set(float64(stat.ModTime().UnixNano()) / 1e9)
|
|
h.Unlock()
|
|
}
|
|
|
|
func (h *Hostsfile) initInline(inline []string) {
|
|
if len(inline) == 0 {
|
|
return
|
|
}
|
|
|
|
h.inline = h.parseSource(strings.NewReader(strings.Join(inline, "\n")), "Inline hosts entries")
|
|
}
|
|
|
|
// maxFieldSize bounds the memory used while assembling a single field that
|
|
// spans several reads. A DNS name is at most 255 octets, so a longer field can
|
|
// never yield a usable entry and is discarded instead of being buffered.
|
|
const maxFieldSize = 1024
|
|
|
|
// Parse reads the hostsfile and populates the byName and addr maps.
|
|
func (h *Hostsfile) parse(r io.Reader) *Map {
|
|
return h.parseSource(r, "Hosts file "+strconv.Quote(h.path))
|
|
}
|
|
|
|
// parseSource is parse with an explicit source name, so that entries inlined in
|
|
// the Corefile are not reported as coming from the hosts file.
|
|
//
|
|
// Lines are read with a bufio.Reader and parsed field by field as the data
|
|
// arrives, so a line of any length is handled with a fixed amount of memory
|
|
// and never aborts the parse of the entries that follow it.
|
|
func (h *Hostsfile) parseSource(r io.Reader, src string) *Map {
|
|
hmap := newMap()
|
|
p := lineParser{h: h, hmap: hmap, src: src, line: 1}
|
|
|
|
reader := bufio.NewReader(r)
|
|
for {
|
|
chunk, err := reader.ReadSlice('\n')
|
|
// The slice returned by ReadSlice is only valid until the next read,
|
|
// so feed consumes it before looping. ErrBufferFull means the line
|
|
// continues in the next chunk.
|
|
p.feed(chunk, err != bufio.ErrBufferFull)
|
|
if err == nil || err == bufio.ErrBufferFull {
|
|
continue
|
|
}
|
|
if err != io.EOF {
|
|
log.Errorf("Failed to parse hosts file %q: %v", h.path, err)
|
|
}
|
|
return hmap
|
|
}
|
|
}
|
|
|
|
// lineParser turns a stream of chunks into hosts file entries. It keeps only
|
|
// the current field, so its memory use does not grow with the line length.
|
|
type lineParser struct {
|
|
h *Hostsfile
|
|
hmap *Map
|
|
src string // where the entries came from, for diagnostics
|
|
|
|
field []byte // the field being assembled, possibly spanning chunks
|
|
oversized bool // the current field exceeded maxFieldSize and is dropped
|
|
index int // number of fields already seen on this line
|
|
line int // 1-based number of the line being parsed, for diagnostics
|
|
comment bool // the rest of this line is a comment
|
|
addr net.IP // address of the current line, nil if unusable
|
|
family int
|
|
}
|
|
|
|
// feed consumes one chunk of the current line. last reports whether the chunk
|
|
// ends the line.
|
|
func (p *lineParser) feed(chunk []byte, last bool) {
|
|
if !p.comment {
|
|
if i := bytes.IndexByte(chunk, '#'); i >= 0 {
|
|
// Discard comments.
|
|
chunk = chunk[:i]
|
|
p.comment = true
|
|
}
|
|
p.scan(chunk, last || p.comment)
|
|
}
|
|
if last {
|
|
p.index, p.comment, p.addr = 0, false, nil
|
|
p.line++
|
|
}
|
|
}
|
|
|
|
// scan splits a chunk into fields. A field at the end of the chunk is only
|
|
// complete if terminal is set, otherwise it continues in the next chunk.
|
|
func (p *lineParser) scan(b []byte, terminal bool) {
|
|
for len(b) > 0 {
|
|
i := 0
|
|
for i < len(b) && isSpace(b[i]) {
|
|
i++
|
|
}
|
|
if i > 0 {
|
|
// Whitespace terminates the field before it.
|
|
p.emit()
|
|
b = b[i:]
|
|
continue
|
|
}
|
|
j := 0
|
|
for j < len(b) && !isSpace(b[j]) {
|
|
j++
|
|
}
|
|
p.append(b[:j])
|
|
b = b[j:]
|
|
}
|
|
if terminal {
|
|
p.emit()
|
|
}
|
|
}
|
|
|
|
// append extends the current field, dropping it once it grows beyond any
|
|
// length a DNS name can have.
|
|
func (p *lineParser) append(b []byte) {
|
|
if p.oversized {
|
|
return
|
|
}
|
|
if len(p.field)+len(b) > maxFieldSize {
|
|
p.oversized = true
|
|
p.field = p.field[:0]
|
|
return
|
|
}
|
|
p.field = append(p.field, b...)
|
|
}
|
|
|
|
// emit handles a completed field. It is a no-op when no field is pending.
|
|
func (p *lineParser) emit() {
|
|
if len(p.field) == 0 && !p.oversized {
|
|
return
|
|
}
|
|
// field aliases p.field's storage, which is reused by the next append; it
|
|
// is only read below, before any further append happens.
|
|
field, oversized := p.field, p.oversized
|
|
p.field, p.oversized = p.field[:0], false
|
|
p.index++
|
|
|
|
if p.index == 1 {
|
|
// The first field is the address; without it the line is unusable.
|
|
if oversized {
|
|
log.Errorf("%s, line %d: address longer than %d bytes, dropping the line",
|
|
p.src, p.line, maxFieldSize)
|
|
return
|
|
}
|
|
p.addr = parseIP(string(field))
|
|
if p.addr == nil {
|
|
return
|
|
}
|
|
if p.addr.To4() != nil {
|
|
p.family = 1
|
|
} else {
|
|
p.family = 2
|
|
}
|
|
return
|
|
}
|
|
if p.addr == nil {
|
|
return
|
|
}
|
|
if oversized {
|
|
log.Errorf("%s, line %d: name longer than %d bytes, dropping the name",
|
|
p.src, p.line, maxFieldSize)
|
|
return
|
|
}
|
|
p.addName(string(field))
|
|
}
|
|
|
|
func (p *lineParser) addName(field string) {
|
|
name := plugin.Name(field).Normalize()
|
|
if !plugin.Zones(p.h.Origins).Contains(name) {
|
|
// name is not in Origins
|
|
return
|
|
}
|
|
if isWildcardName(name) {
|
|
switch p.family {
|
|
case 1:
|
|
p.hmap.wildName4[name] = append(p.hmap.wildName4[name], p.addr)
|
|
case 2:
|
|
p.hmap.wildName6[name] = append(p.hmap.wildName6[name], p.addr)
|
|
}
|
|
return
|
|
}
|
|
switch p.family {
|
|
case 1:
|
|
p.hmap.name4[name] = append(p.hmap.name4[name], p.addr)
|
|
case 2:
|
|
p.hmap.name6[name] = append(p.hmap.name6[name], p.addr)
|
|
default:
|
|
return
|
|
}
|
|
if !p.h.options.autoReverse {
|
|
return
|
|
}
|
|
key := p.addr.String()
|
|
p.hmap.addr[key] = append(p.hmap.addr[key], name)
|
|
}
|
|
|
|
func isSpace(c byte) bool {
|
|
switch c {
|
|
case ' ', '\t', '\n', '\v', '\f', '\r':
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (h *Hostsfile) lookupStaticHostLocked(m, wild map[string][]net.IP, host string) []net.IP {
|
|
if ips, ok := m[host]; ok {
|
|
ipsCp := make([]net.IP, len(ips))
|
|
copy(ipsCp, ips)
|
|
return ipsCp
|
|
}
|
|
if pattern := replaceWithAsteriskLabel(host); pattern != "" {
|
|
if ips, ok := wild[pattern]; ok {
|
|
ipsCp := make([]net.IP, len(ips))
|
|
copy(ipsCp, ips)
|
|
return ipsCp
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (h *Hostsfile) lookupStaticHostFamily(host string, v4 bool) []net.IP {
|
|
host = strings.ToLower(host)
|
|
|
|
h.RLock()
|
|
defer h.RUnlock()
|
|
|
|
// h.hmap and h.inline must be read under the lock: readHosts swaps h.hmap
|
|
// under h.Lock() on every reload.
|
|
var ip1, ip2 []net.IP
|
|
if v4 {
|
|
ip1 = h.lookupStaticHostLocked(h.hmap.name4, h.hmap.wildName4, host)
|
|
ip2 = h.lookupStaticHostLocked(h.inline.name4, h.inline.wildName4, host)
|
|
} else {
|
|
ip1 = h.lookupStaticHostLocked(h.hmap.name6, h.hmap.wildName6, host)
|
|
ip2 = h.lookupStaticHostLocked(h.inline.name6, h.inline.wildName6, host)
|
|
}
|
|
return append(ip1, ip2...)
|
|
}
|
|
|
|
// LookupStaticHostV4 looks up the IPv4 addresses for the given host from the hosts file.
|
|
func (h *Hostsfile) LookupStaticHostV4(host string) []net.IP {
|
|
return h.lookupStaticHostFamily(host, true)
|
|
}
|
|
|
|
// LookupStaticHostV6 looks up the IPv6 addresses for the given host from the hosts file.
|
|
func (h *Hostsfile) LookupStaticHostV6(host string) []net.IP {
|
|
return h.lookupStaticHostFamily(host, false)
|
|
}
|
|
|
|
// LookupStaticAddr looks up the hosts for the given address from the hosts file.
|
|
func (h *Hostsfile) LookupStaticAddr(addr string) []string {
|
|
addr = parseIP(addr).String()
|
|
if addr == "" {
|
|
return nil
|
|
}
|
|
|
|
h.RLock()
|
|
defer h.RUnlock()
|
|
hosts1 := h.hmap.addr[addr]
|
|
hosts2 := h.inline.addr[addr]
|
|
|
|
if len(hosts1) == 0 && len(hosts2) == 0 {
|
|
return nil
|
|
}
|
|
|
|
hostsCp := make([]string, len(hosts1)+len(hosts2))
|
|
copy(hostsCp, hosts1)
|
|
copy(hostsCp[len(hosts1):], hosts2)
|
|
return hostsCp
|
|
}
|