Files
sing-box-extended-mirror/dns/transport/local/systemconfig/source_resolv.go
T

177 lines
3.9 KiB
Go

//go:build !windows && !(darwin && cgo)
package systemconfig
import (
"bufio"
"context"
"net/netip"
"os"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
M "github.com/sagernet/sing/common/metadata"
mDNS "github.com/miekg/dns"
)
const resolvConfPath = "/etc/resolv.conf"
type Source struct {
updateAccess sync.Mutex
lastChecked time.Time
current atomic.Pointer[resolvConfig]
}
type resolvConfig struct {
config *Config
mtime time.Time
noReload bool
}
func NewSource(_ context.Context) *Source {
source := &Source{lastChecked: time.Now()}
source.current.Store(readResolvConfig(resolvConfPath))
return source
}
func (s *Source) Configuration() *Config {
s.tryUpdate()
return s.current.Load().config
}
func (s *Source) tryUpdate() {
if s.current.Load().noReload {
return
}
if !s.updateAccess.TryLock() {
return
}
defer s.updateAccess.Unlock()
now := time.Now()
if s.lastChecked.After(now.Add(-5 * time.Second)) {
return
}
s.lastChecked = now
var mtime time.Time
fileInfo, err := os.Stat(resolvConfPath)
if err == nil {
mtime = fileInfo.ModTime()
}
current := s.current.Load()
if mtime.Equal(current.mtime) {
return
}
updated := readResolvConfig(resolvConfPath)
if updated.config.Equal(current.config) {
updated.config = current.config
}
s.current.Store(updated)
}
func (s *Source) Reset() {
s.updateAccess.Lock()
s.lastChecked = time.Time{}
s.updateAccess.Unlock()
}
func (s *Source) Close() error {
return nil
}
func readResolvConfig(path string) *resolvConfig {
config := &Config{
Ndots: 1,
Timeout: 5 * time.Second,
Attempts: 2,
}
result := &resolvConfig{config: config}
file, err := os.Open(path)
if err != nil {
config.Servers = defaultServers
config.Search = defaultSearch()
return result
}
defer file.Close()
fileInfo, err := file.Stat()
if err != nil {
config.Servers = defaultServers
config.Search = defaultSearch()
return result
}
result.mtime = fileInfo.ModTime()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, ";") || strings.HasPrefix(line, "#") {
continue
}
fields := strings.Fields(line)
if len(fields) < 1 {
continue
}
switch fields[0] {
case "nameserver":
if len(fields) > 1 && len(config.Servers) < 3 {
serverAddr, parseErr := netip.ParseAddr(fields[1])
if parseErr == nil {
config.Servers = append(config.Servers, M.SocksaddrFrom(serverAddr, 53))
}
}
case "domain":
if len(fields) > 1 {
config.Search = []string{mDNS.Fqdn(fields[1])}
}
case "search":
config.Search = make([]string, 0, len(fields)-1)
for _, searchDomain := range fields[1:] {
name := mDNS.Fqdn(searchDomain)
if name == "." {
continue
}
config.Search = append(config.Search, name)
}
case "options":
for _, option := range fields[1:] {
switch {
case strings.HasPrefix(option, "ndots:"):
value, parseErr := strconv.Atoi(option[len("ndots:"):])
if parseErr == nil {
config.Ndots = min(max(value, 0), 15)
}
case strings.HasPrefix(option, "timeout:"):
value, parseErr := strconv.Atoi(option[len("timeout:"):])
if parseErr == nil {
config.Timeout = time.Duration(max(value, 1)) * time.Second
}
case strings.HasPrefix(option, "attempts:"):
value, parseErr := strconv.Atoi(option[len("attempts:"):])
if parseErr == nil {
config.Attempts = max(value, 1)
}
case option == "rotate":
config.Rotate = true
case option == "single-request" || option == "single-request-reopen":
config.SingleRequest = true
case option == "use-vc" || option == "usevc" || option == "tcp":
config.UseTCP = true
case option == "trust-ad":
config.TrustAD = true
case option == "no-reload":
result.noReload = true
}
}
}
}
if len(config.Servers) == 0 {
config.Servers = defaultServers
}
if len(config.Search) == 0 {
config.Search = defaultSearch()
}
return result
}