Files
coredns/plugin/hosts/hostsfile.go
Paco Cartones 85aa27cd9c plugin/hosts: don't drop entries after an over-long line (#8496)
bufio.Scanner stops at the first line longer than its 64KiB default
buffer and reports bufio.ErrTooLong from Err(). parse() never checked
Err(), so that line and every entry after it were dropped silently: the
hosts file simply looked shorter than it is, with nothing in the log.

Raise the scanner's limit to 1MiB (the scanner still grows its buffer
lazily, so nothing is preallocated up front) and log an error if the
scan does stop early, so the truncation is at least visible.

Signed-off-by: Paco Cartones <pacocartones@users.noreply.github.com>
Co-authored-by: Paco Cartones <pacocartones@users.noreply.github.com>
2026-09-01 00:01:04 -07:00

306 lines
7.2 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"
"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.parse(strings.NewReader(strings.Join(inline, "\n")))
}
// maxLineSize is the largest hosts file line we are willing to parse. A line
// can legitimately be long when many names share a single address, so this is
// well above bufio.Scanner's 64KiB default, but still bounded.
const maxLineSize = 1024 * 1024
// Parse reads the hostsfile and populates the byName and addr maps.
func (h *Hostsfile) parse(r io.Reader) *Map {
hmap := newMap()
scanner := bufio.NewScanner(r)
// The scanner grows its buffer as needed; only raise the limit at which it
// gives up, otherwise a single long line aborts the scan and every entry
// after it is dropped.
scanner.Buffer(nil, maxLineSize)
for scanner.Scan() {
line := scanner.Bytes()
if i := bytes.Index(line, []byte{'#'}); i >= 0 {
// Discard comments.
line = line[0:i]
}
f := bytes.Fields(line)
if len(f) < 2 {
continue
}
addr := parseIP(string(f[0]))
if addr == nil {
continue
}
var family int
if addr.To4() != nil {
family = 1
} else {
family = 2
}
for i := 1; i < len(f); i++ {
name := plugin.Name(string(f[i])).Normalize()
if plugin.Zones(h.Origins).Matches(name) == "" {
// name is not in Origins
continue
}
if isWildcardName(name) {
switch family {
case 1:
hmap.wildName4[name] = append(hmap.wildName4[name], addr)
case 2:
hmap.wildName6[name] = append(hmap.wildName6[name], addr)
}
continue
}
switch family {
case 1:
hmap.name4[name] = append(hmap.name4[name], addr)
case 2:
hmap.name6[name] = append(hmap.name6[name], addr)
default:
continue
}
if !h.options.autoReverse {
continue
}
hmap.addr[addr.String()] = append(hmap.addr[addr.String()], name)
}
}
if err := scanner.Err(); err != nil {
// Entries after the failing line have not been read.
log.Errorf("Failed to parse hosts file %q: %v", h.path, err)
}
return hmap
}
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
}