mirror of
https://github.com/tailscale/tailscale.git
synced 2025-08-12 05:37:32 +00:00
wgengine/router/dns: move to net/dns.
Preparation for merging the APIs and whatnot. Signed-off-by: David Anderson <danderson@tailscale.com>
This commit is contained in:

committed by
Dave Anderson

parent
8432999835
commit
9f7f2af008
77
net/dns/config.go
Normal file
77
net/dns/config.go
Normal file
@@ -0,0 +1,77 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"inet.af/netaddr"
|
||||
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// Config is the set of parameters that uniquely determine
|
||||
// the state to which a manager should bring system DNS settings.
|
||||
type Config struct {
|
||||
// Nameservers are the IP addresses of the nameservers to use.
|
||||
Nameservers []netaddr.IP
|
||||
// Domains are the search domains to use.
|
||||
Domains []string
|
||||
// PerDomain indicates whether it is preferred to use Nameservers
|
||||
// only for DNS queries for subdomains of Domains.
|
||||
// Note that Nameservers may still be applied to all queries
|
||||
// if the manager does not support per-domain settings.
|
||||
PerDomain bool
|
||||
// Proxied indicates whether DNS requests are proxied through a dns.Resolver.
|
||||
// This enables MagicDNS.
|
||||
Proxied bool
|
||||
}
|
||||
|
||||
// Equal determines whether its argument and receiver
|
||||
// represent equivalent DNS configurations (then DNS reconfig is a no-op).
|
||||
func (lhs Config) Equal(rhs Config) bool {
|
||||
if lhs.Proxied != rhs.Proxied || lhs.PerDomain != rhs.PerDomain {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(lhs.Nameservers) != len(rhs.Nameservers) {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(lhs.Domains) != len(rhs.Domains) {
|
||||
return false
|
||||
}
|
||||
|
||||
// With how we perform resolution order shouldn't matter,
|
||||
// but it is unlikely that we will encounter different orders.
|
||||
for i, server := range lhs.Nameservers {
|
||||
if rhs.Nameservers[i] != server {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// The order of domains, on the other hand, is significant.
|
||||
for i, domain := range lhs.Domains {
|
||||
if rhs.Domains[i] != domain {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// ManagerConfig is the set of parameters from which
|
||||
// a manager implementation is chosen and initialized.
|
||||
type ManagerConfig struct {
|
||||
// Logf is the logger for the manager to use.
|
||||
// It is wrapped with a "dns: " prefix.
|
||||
Logf logger.Logf
|
||||
// InterfaceName is the name of the interface with which DNS settings should be associated.
|
||||
InterfaceName string
|
||||
// Cleanup indicates that the manager is created for cleanup only.
|
||||
// A no-op manager will be instantiated if the system needs no cleanup.
|
||||
Cleanup bool
|
||||
// PerDomain indicates that a manager capable of per-domain configuration is preferred.
|
||||
// Certain managers are per-domain only; they will not be considered if this is false.
|
||||
PerDomain bool
|
||||
}
|
188
net/dns/direct.go
Normal file
188
net/dns/direct.go
Normal file
@@ -0,0 +1,188 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build linux freebsd openbsd
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/atomicfile"
|
||||
)
|
||||
|
||||
const (
|
||||
tsConf = "/etc/resolv.tailscale.conf"
|
||||
backupConf = "/etc/resolv.pre-tailscale-backup.conf"
|
||||
resolvConf = "/etc/resolv.conf"
|
||||
)
|
||||
|
||||
// writeResolvConf writes DNS configuration in resolv.conf format to the given writer.
|
||||
func writeResolvConf(w io.Writer, servers []netaddr.IP, domains []string) {
|
||||
io.WriteString(w, "# resolv.conf(5) file generated by tailscale\n")
|
||||
io.WriteString(w, "# DO NOT EDIT THIS FILE BY HAND -- CHANGES WILL BE OVERWRITTEN\n\n")
|
||||
for _, ns := range servers {
|
||||
io.WriteString(w, "nameserver ")
|
||||
io.WriteString(w, ns.String())
|
||||
io.WriteString(w, "\n")
|
||||
}
|
||||
if len(domains) > 0 {
|
||||
io.WriteString(w, "search")
|
||||
for _, domain := range domains {
|
||||
io.WriteString(w, " ")
|
||||
io.WriteString(w, domain)
|
||||
}
|
||||
io.WriteString(w, "\n")
|
||||
}
|
||||
}
|
||||
|
||||
// readResolvConf reads DNS configuration from /etc/resolv.conf.
|
||||
func readResolvConf() (Config, error) {
|
||||
var config Config
|
||||
|
||||
f, err := os.Open("/etc/resolv.conf")
|
||||
if err != nil {
|
||||
return config, err
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
|
||||
if strings.HasPrefix(line, "nameserver") {
|
||||
nameserver := strings.TrimPrefix(line, "nameserver")
|
||||
nameserver = strings.TrimSpace(nameserver)
|
||||
ip, err := netaddr.ParseIP(nameserver)
|
||||
if err != nil {
|
||||
return config, err
|
||||
}
|
||||
config.Nameservers = append(config.Nameservers, ip)
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(line, "search") {
|
||||
domain := strings.TrimPrefix(line, "search")
|
||||
domain = strings.TrimSpace(domain)
|
||||
config.Domains = append(config.Domains, domain)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// isResolvedRunning reports whether systemd-resolved is running on the system,
|
||||
// even if it is not managing the system DNS settings.
|
||||
func isResolvedRunning() bool {
|
||||
if runtime.GOOS != "linux" {
|
||||
return false
|
||||
}
|
||||
|
||||
// systemd-resolved is never installed without systemd.
|
||||
_, err := exec.LookPath("systemctl")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// is-active exits with code 3 if the service is not active.
|
||||
err = exec.Command("systemctl", "is-active", "systemd-resolved.service").Run()
|
||||
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// directManager is a managerImpl which replaces /etc/resolv.conf with a file
|
||||
// generated from the given configuration, creating a backup of its old state.
|
||||
//
|
||||
// This way of configuring DNS is precarious, since it does not react
|
||||
// to the disappearance of the Tailscale interface.
|
||||
// The caller must call Down before program shutdown
|
||||
// or as cleanup if the program terminates unexpectedly.
|
||||
type directManager struct{}
|
||||
|
||||
func newDirectManager(mconfig ManagerConfig) managerImpl {
|
||||
return directManager{}
|
||||
}
|
||||
|
||||
// Up implements managerImpl.
|
||||
func (m directManager) Up(config Config) error {
|
||||
// Write the tsConf file.
|
||||
buf := new(bytes.Buffer)
|
||||
writeResolvConf(buf, config.Nameservers, config.Domains)
|
||||
if err := atomicfile.WriteFile(tsConf, buf.Bytes(), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if linkPath, err := os.Readlink(resolvConf); err != nil {
|
||||
// Remove any old backup that may exist.
|
||||
os.Remove(backupConf)
|
||||
|
||||
// Backup the existing /etc/resolv.conf file.
|
||||
contents, err := ioutil.ReadFile(resolvConf)
|
||||
// If the original did not exist, still back up an empty file.
|
||||
// The presence of a backup file is the way we know that Up ran.
|
||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
if err := atomicfile.WriteFile(backupConf, contents, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if linkPath != tsConf {
|
||||
// Backup the existing symlink.
|
||||
os.Remove(backupConf)
|
||||
if err := os.Symlink(linkPath, backupConf); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// Nothing to do, resolvConf already points to tsConf.
|
||||
return nil
|
||||
}
|
||||
|
||||
os.Remove(resolvConf)
|
||||
if err := os.Symlink(tsConf, resolvConf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if isResolvedRunning() {
|
||||
exec.Command("systemctl", "restart", "systemd-resolved.service").Run() // Best-effort.
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Down implements managerImpl.
|
||||
func (m directManager) Down() error {
|
||||
if _, err := os.Stat(backupConf); err != nil {
|
||||
// If the backup file does not exist, then Up never ran successfully.
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if ln, err := os.Readlink(resolvConf); err != nil {
|
||||
return err
|
||||
} else if ln != tsConf {
|
||||
return fmt.Errorf("resolv.conf is not a symlink to %s", tsConf)
|
||||
}
|
||||
if err := os.Rename(backupConf, resolvConf); err != nil {
|
||||
return err
|
||||
}
|
||||
os.Remove(tsConf)
|
||||
|
||||
if isResolvedRunning() {
|
||||
exec.Command("systemctl", "restart", "systemd-resolved.service").Run() // Best-effort.
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
100
net/dns/manager.go
Normal file
100
net/dns/manager.go
Normal file
@@ -0,0 +1,100 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// We use file-ignore below instead of ignore because on some platforms,
|
||||
// the lint exception is necessary and on others it is not,
|
||||
// and plain ignore complains if the exception is unnecessary.
|
||||
|
||||
//lint:file-ignore U1000 reconfigTimeout is used on some platforms but not others
|
||||
|
||||
// reconfigTimeout is the time interval within which Manager.{Up,Down} should complete.
|
||||
//
|
||||
// This is particularly useful because certain conditions can cause indefinite hangs
|
||||
// (such as improper dbus auth followed by contextless dbus.Object.Call).
|
||||
// Such operations should be wrapped in a timeout context.
|
||||
const reconfigTimeout = time.Second
|
||||
|
||||
type managerImpl interface {
|
||||
// Up updates system DNS settings to match the given configuration.
|
||||
Up(Config) error
|
||||
// Down undoes the effects of Up.
|
||||
// It is idempotent and performs no action if Up has never been called.
|
||||
Down() error
|
||||
}
|
||||
|
||||
// Manager manages system DNS settings.
|
||||
type Manager struct {
|
||||
logf logger.Logf
|
||||
|
||||
impl managerImpl
|
||||
|
||||
config Config
|
||||
mconfig ManagerConfig
|
||||
}
|
||||
|
||||
// NewManagers created a new manager from the given config.
|
||||
func NewManager(mconfig ManagerConfig) *Manager {
|
||||
mconfig.Logf = logger.WithPrefix(mconfig.Logf, "dns: ")
|
||||
m := &Manager{
|
||||
logf: mconfig.Logf,
|
||||
impl: newManager(mconfig),
|
||||
|
||||
config: Config{PerDomain: mconfig.PerDomain},
|
||||
mconfig: mconfig,
|
||||
}
|
||||
|
||||
m.logf("using %T", m.impl)
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *Manager) Set(config Config) error {
|
||||
if config.Equal(m.config) {
|
||||
return nil
|
||||
}
|
||||
|
||||
m.logf("Set: %+v", config)
|
||||
|
||||
if len(config.Nameservers) == 0 {
|
||||
err := m.impl.Down()
|
||||
// If we save the config, we will not retry next time. Only do this on success.
|
||||
if err == nil {
|
||||
m.config = config
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Switching to and from per-domain mode may require a change of manager.
|
||||
if config.PerDomain != m.config.PerDomain {
|
||||
if err := m.impl.Down(); err != nil {
|
||||
return err
|
||||
}
|
||||
m.mconfig.PerDomain = config.PerDomain
|
||||
m.impl = newManager(m.mconfig)
|
||||
m.logf("switched to %T", m.impl)
|
||||
}
|
||||
|
||||
err := m.impl.Up(config)
|
||||
// If we save the config, we will not retry next time. Only do this on success.
|
||||
if err == nil {
|
||||
m.config = config
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *Manager) Up() error {
|
||||
return m.impl.Up(m.config)
|
||||
}
|
||||
|
||||
func (m *Manager) Down() error {
|
||||
return m.impl.Down()
|
||||
}
|
14
net/dns/manager_default.go
Normal file
14
net/dns/manager_default.go
Normal file
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build !linux,!freebsd,!openbsd,!windows
|
||||
|
||||
package dns
|
||||
|
||||
func newManager(mconfig ManagerConfig) managerImpl {
|
||||
// TODO(dmytro): on darwin, we should use a macOS-specific method such as scutil.
|
||||
// This is currently not implemented. Editing /etc/resolv.conf does not work,
|
||||
// as most applications use the system resolver, which disregards it.
|
||||
return newNoopManager(mconfig)
|
||||
}
|
14
net/dns/manager_freebsd.go
Normal file
14
net/dns/manager_freebsd.go
Normal file
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package dns
|
||||
|
||||
func newManager(mconfig ManagerConfig) managerImpl {
|
||||
switch {
|
||||
case isResolvconfActive():
|
||||
return newResolvconfManager(mconfig)
|
||||
default:
|
||||
return newDirectManager(mconfig)
|
||||
}
|
||||
}
|
27
net/dns/manager_linux.go
Normal file
27
net/dns/manager_linux.go
Normal file
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package dns
|
||||
|
||||
func newManager(mconfig ManagerConfig) managerImpl {
|
||||
switch {
|
||||
// systemd-resolved should only activate per-domain.
|
||||
case isResolvedActive() && mconfig.PerDomain:
|
||||
if mconfig.Cleanup {
|
||||
return newNoopManager(mconfig)
|
||||
} else {
|
||||
return newResolvedManager(mconfig)
|
||||
}
|
||||
case isNMActive():
|
||||
if mconfig.Cleanup {
|
||||
return newNoopManager(mconfig)
|
||||
} else {
|
||||
return newNMManager(mconfig)
|
||||
}
|
||||
case isResolvconfActive():
|
||||
return newResolvconfManager(mconfig)
|
||||
default:
|
||||
return newDirectManager(mconfig)
|
||||
}
|
||||
}
|
9
net/dns/manager_openbsd.go
Normal file
9
net/dns/manager_openbsd.go
Normal file
@@ -0,0 +1,9 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package dns
|
||||
|
||||
func newManager(mconfig ManagerConfig) managerImpl {
|
||||
return newDirectManager(mconfig)
|
||||
}
|
118
net/dns/manager_windows.go
Normal file
118
net/dns/manager_windows.go
Normal file
@@ -0,0 +1,118 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/windows/registry"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
ipv4RegBase = `SYSTEM\CurrentControlSet\Services\Tcpip\Parameters`
|
||||
ipv6RegBase = `SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters`
|
||||
)
|
||||
|
||||
type windowsManager struct {
|
||||
logf logger.Logf
|
||||
guid string
|
||||
}
|
||||
|
||||
func newManager(mconfig ManagerConfig) managerImpl {
|
||||
return windowsManager{
|
||||
logf: mconfig.Logf,
|
||||
guid: mconfig.InterfaceName,
|
||||
}
|
||||
}
|
||||
|
||||
// keyOpenTimeout is how long we wait for a registry key to
|
||||
// appear. For some reason, registry keys tied to ephemeral interfaces
|
||||
// can take a long while to appear after interface creation, and we
|
||||
// can end up racing with that.
|
||||
const keyOpenTimeout = 20 * time.Second
|
||||
|
||||
func setRegistryString(path, name, value string) error {
|
||||
key, err := openKeyWait(registry.LOCAL_MACHINE, path, registry.SET_VALUE, keyOpenTimeout)
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening %s: %w", path, err)
|
||||
}
|
||||
defer key.Close()
|
||||
|
||||
err = key.SetStringValue(name, value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting %s[%s]: %w", path, name, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m windowsManager) setNameservers(basePath string, nameservers []string) error {
|
||||
path := fmt.Sprintf(`%s\Interfaces\%s`, basePath, m.guid)
|
||||
value := strings.Join(nameservers, ",")
|
||||
return setRegistryString(path, "NameServer", value)
|
||||
}
|
||||
|
||||
func (m windowsManager) setDomains(basePath string, domains []string) error {
|
||||
path := fmt.Sprintf(`%s\Interfaces\%s`, basePath, m.guid)
|
||||
value := strings.Join(domains, ",")
|
||||
return setRegistryString(path, "SearchList", value)
|
||||
}
|
||||
|
||||
func (m windowsManager) Up(config Config) error {
|
||||
var ipsv4 []string
|
||||
var ipsv6 []string
|
||||
|
||||
for _, ip := range config.Nameservers {
|
||||
if ip.Is4() {
|
||||
ipsv4 = append(ipsv4, ip.String())
|
||||
} else {
|
||||
ipsv6 = append(ipsv6, ip.String())
|
||||
}
|
||||
}
|
||||
|
||||
if err := m.setNameservers(ipv4RegBase, ipsv4); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := m.setDomains(ipv4RegBase, config.Domains); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := m.setNameservers(ipv6RegBase, ipsv6); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := m.setDomains(ipv6RegBase, config.Domains); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Force DNS re-registration in Active Directory. What we actually
|
||||
// care about is that this command invokes the undocumented hidden
|
||||
// function that forces Windows to notice that adapter settings
|
||||
// have changed, which makes the DNS settings actually take
|
||||
// effect.
|
||||
//
|
||||
// This command can take a few seconds to run, so run it async, best effort.
|
||||
go func() {
|
||||
t0 := time.Now()
|
||||
m.logf("running ipconfig /registerdns ...")
|
||||
cmd := exec.Command("ipconfig", "/registerdns")
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||
d := time.Since(t0).Round(time.Millisecond)
|
||||
if err := cmd.Run(); err != nil {
|
||||
m.logf("error running ipconfig /registerdns after %v: %v", d, err)
|
||||
} else {
|
||||
m.logf("ran ipconfig /registerdns in %v", d)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m windowsManager) Down() error {
|
||||
return m.Up(Config{Nameservers: nil, Domains: nil})
|
||||
}
|
205
net/dns/nm.go
Normal file
205
net/dns/nm.go
Normal file
@@ -0,0 +1,205 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build linux
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
"tailscale.com/util/endian"
|
||||
)
|
||||
|
||||
// isNMActive determines if NetworkManager is currently managing system DNS settings.
|
||||
func isNMActive() bool {
|
||||
// This is somewhat tricky because NetworkManager supports a number
|
||||
// of DNS configuration modes. In all cases, we expect it to be installed
|
||||
// and /etc/resolv.conf to contain a mention of NetworkManager in the comments.
|
||||
_, err := exec.LookPath("NetworkManager")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
f, err := os.Open("/etc/resolv.conf")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Bytes()
|
||||
// Look for the word "NetworkManager" until comments end.
|
||||
if len(line) > 0 && line[0] != '#' {
|
||||
return false
|
||||
}
|
||||
if bytes.Contains(line, []byte("NetworkManager")) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// nmManager uses the NetworkManager DBus API.
|
||||
type nmManager struct {
|
||||
interfaceName string
|
||||
}
|
||||
|
||||
func newNMManager(mconfig ManagerConfig) managerImpl {
|
||||
return nmManager{
|
||||
interfaceName: mconfig.InterfaceName,
|
||||
}
|
||||
}
|
||||
|
||||
type nmConnectionSettings map[string]map[string]dbus.Variant
|
||||
|
||||
// Up implements managerImpl.
|
||||
func (m nmManager) Up(config Config) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), reconfigTimeout)
|
||||
defer cancel()
|
||||
|
||||
// conn is a shared connection whose lifecycle is managed by the dbus package.
|
||||
// We should not interfere with that by closing it.
|
||||
conn, err := dbus.SystemBus()
|
||||
if err != nil {
|
||||
return fmt.Errorf("connecting to system bus: %w", err)
|
||||
}
|
||||
|
||||
// This is how we get at the DNS settings:
|
||||
//
|
||||
// org.freedesktop.NetworkManager
|
||||
// |
|
||||
// [GetDeviceByIpIface]
|
||||
// |
|
||||
// v
|
||||
// org.freedesktop.NetworkManager.Device <--------\
|
||||
// (describes a network interface) |
|
||||
// | |
|
||||
// [GetAppliedConnection] [Reapply]
|
||||
// | |
|
||||
// v |
|
||||
// org.freedesktop.NetworkManager.Connection |
|
||||
// (connection settings) ------/
|
||||
// contains {dns, dns-priority, dns-search}
|
||||
//
|
||||
// Ref: https://developer.gnome.org/NetworkManager/stable/settings-ipv4.html.
|
||||
|
||||
nm := conn.Object(
|
||||
"org.freedesktop.NetworkManager",
|
||||
dbus.ObjectPath("/org/freedesktop/NetworkManager"),
|
||||
)
|
||||
|
||||
var devicePath dbus.ObjectPath
|
||||
err = nm.CallWithContext(
|
||||
ctx, "org.freedesktop.NetworkManager.GetDeviceByIpIface", 0,
|
||||
m.interfaceName,
|
||||
).Store(&devicePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getDeviceByIpIface: %w", err)
|
||||
}
|
||||
device := conn.Object("org.freedesktop.NetworkManager", devicePath)
|
||||
|
||||
var (
|
||||
settings nmConnectionSettings
|
||||
version uint64
|
||||
)
|
||||
err = device.CallWithContext(
|
||||
ctx, "org.freedesktop.NetworkManager.Device.GetAppliedConnection", 0,
|
||||
uint32(0),
|
||||
).Store(&settings, &version)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getAppliedConnection: %w", err)
|
||||
}
|
||||
|
||||
// Frustratingly, NetworkManager represents IPv4 addresses as uint32s,
|
||||
// although IPv6 addresses are represented as byte arrays.
|
||||
// Perform the conversion here.
|
||||
var (
|
||||
dnsv4 []uint32
|
||||
dnsv6 [][]byte
|
||||
)
|
||||
for _, ip := range config.Nameservers {
|
||||
b := ip.As16()
|
||||
if ip.Is4() {
|
||||
dnsv4 = append(dnsv4, endian.Native.Uint32(b[12:]))
|
||||
} else {
|
||||
dnsv6 = append(dnsv6, b[:])
|
||||
}
|
||||
}
|
||||
|
||||
ipv4Map := settings["ipv4"]
|
||||
ipv4Map["dns"] = dbus.MakeVariant(dnsv4)
|
||||
ipv4Map["dns-search"] = dbus.MakeVariant(config.Domains)
|
||||
// We should only request priority if we have nameservers to set.
|
||||
if len(dnsv4) == 0 {
|
||||
ipv4Map["dns-priority"] = dbus.MakeVariant(100)
|
||||
} else {
|
||||
// dns-priority = -1 ensures that we have priority
|
||||
// over other interfaces, except those exploiting this same trick.
|
||||
// Ref: https://bugs.launchpad.net/ubuntu/+source/network-manager/+bug/1211110/comments/92.
|
||||
ipv4Map["dns-priority"] = dbus.MakeVariant(-1)
|
||||
}
|
||||
// In principle, we should not need set this to true,
|
||||
// as our interface does not configure any automatic DNS settings (presumably via DHCP).
|
||||
// All the same, better to be safe.
|
||||
ipv4Map["ignore-auto-dns"] = dbus.MakeVariant(true)
|
||||
|
||||
ipv6Map := settings["ipv6"]
|
||||
// This is a hack.
|
||||
// Methods "disabled", "ignore", "link-local" (IPv6 default) prevent us from setting DNS.
|
||||
// It seems that our only recourse is "manual" or "auto".
|
||||
// "manual" requires addresses, so we use "auto", which will assign us a random IPv6 /64.
|
||||
ipv6Map["method"] = dbus.MakeVariant("auto")
|
||||
// Our IPv6 config is a fake, so it should never become the default route.
|
||||
ipv6Map["never-default"] = dbus.MakeVariant(true)
|
||||
// Moreover, we should ignore all autoconfigured routes (hopefully none), as they are bogus.
|
||||
ipv6Map["ignore-auto-routes"] = dbus.MakeVariant(true)
|
||||
|
||||
// Finally, set the actual DNS config.
|
||||
ipv6Map["dns"] = dbus.MakeVariant(dnsv6)
|
||||
ipv6Map["dns-search"] = dbus.MakeVariant(config.Domains)
|
||||
if len(dnsv6) == 0 {
|
||||
ipv6Map["dns-priority"] = dbus.MakeVariant(100)
|
||||
} else {
|
||||
ipv6Map["dns-priority"] = dbus.MakeVariant(-1)
|
||||
}
|
||||
ipv6Map["ignore-auto-dns"] = dbus.MakeVariant(true)
|
||||
|
||||
// deprecatedProperties are the properties in interface settings
|
||||
// that are deprecated by NetworkManager.
|
||||
//
|
||||
// In practice, this means that they are returned for reading,
|
||||
// but submitting a settings object with them present fails
|
||||
// with hard-to-diagnose errors. They must be removed.
|
||||
deprecatedProperties := []string{
|
||||
"addresses", "routes",
|
||||
}
|
||||
|
||||
for _, property := range deprecatedProperties {
|
||||
delete(ipv4Map, property)
|
||||
delete(ipv6Map, property)
|
||||
}
|
||||
|
||||
err = device.CallWithContext(
|
||||
ctx, "org.freedesktop.NetworkManager.Device.Reapply", 0,
|
||||
settings, version, uint32(0),
|
||||
).Store()
|
||||
if err != nil {
|
||||
return fmt.Errorf("reapply: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Down implements managerImpl.
|
||||
func (m nmManager) Down() error {
|
||||
return m.Up(Config{Nameservers: nil, Domains: nil})
|
||||
}
|
17
net/dns/noop.go
Normal file
17
net/dns/noop.go
Normal file
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package dns
|
||||
|
||||
type noopManager struct{}
|
||||
|
||||
// Up implements managerImpl.
|
||||
func (m noopManager) Up(Config) error { return nil }
|
||||
|
||||
// Down implements managerImpl.
|
||||
func (m noopManager) Down() error { return nil }
|
||||
|
||||
func newNoopManager(mconfig ManagerConfig) managerImpl {
|
||||
return noopManager{}
|
||||
}
|
76
net/dns/registry_windows.go
Normal file
76
net/dns/registry_windows.go
Normal file
@@ -0,0 +1,76 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
//
|
||||
// The code in this file originates from https://git.zx2c4.com/wireguard-go:
|
||||
// Copyright (C) 2017-2020 WireGuard LLC. All Rights Reserved.
|
||||
// Copying license: https://git.zx2c4.com/wireguard-go/tree/COPYING
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.org/x/sys/windows/registry"
|
||||
)
|
||||
|
||||
func openKeyWait(k registry.Key, path string, access uint32, timeout time.Duration) (registry.Key, error) {
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
|
||||
deadline := time.Now().Add(timeout)
|
||||
pathSpl := strings.Split(path, "\\")
|
||||
for i := 0; ; i++ {
|
||||
keyName := pathSpl[i]
|
||||
isLast := i+1 == len(pathSpl)
|
||||
|
||||
event, err := windows.CreateEvent(nil, 0, 0, nil)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("windows.CreateEvent: %v", err)
|
||||
}
|
||||
defer windows.CloseHandle(event)
|
||||
|
||||
var key registry.Key
|
||||
for {
|
||||
err = windows.RegNotifyChangeKeyValue(windows.Handle(k), false, windows.REG_NOTIFY_CHANGE_NAME, event, true)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("windows.RegNotifyChangeKeyValue: %v", err)
|
||||
}
|
||||
|
||||
var accessFlags uint32
|
||||
if isLast {
|
||||
accessFlags = access
|
||||
} else {
|
||||
accessFlags = registry.NOTIFY
|
||||
}
|
||||
key, err = registry.OpenKey(k, keyName, accessFlags)
|
||||
if err == windows.ERROR_FILE_NOT_FOUND || err == windows.ERROR_PATH_NOT_FOUND {
|
||||
timeout := time.Until(deadline) / time.Millisecond
|
||||
if timeout < 0 {
|
||||
timeout = 0
|
||||
}
|
||||
s, err := windows.WaitForSingleObject(event, uint32(timeout))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("windows.WaitForSingleObject: %v", err)
|
||||
}
|
||||
if s == uint32(windows.WAIT_TIMEOUT) { // windows.WAIT_TIMEOUT status const is misclassified as error in golang.org/x/sys/windows
|
||||
return 0, fmt.Errorf("timeout waiting for registry key")
|
||||
}
|
||||
} else if err != nil {
|
||||
return 0, fmt.Errorf("registry.OpenKey(%v): %v", path, err)
|
||||
} else {
|
||||
if isLast {
|
||||
return key, nil
|
||||
}
|
||||
defer key.Close()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
k = key
|
||||
}
|
||||
}
|
157
net/dns/resolvconf.go
Normal file
157
net/dns/resolvconf.go
Normal file
@@ -0,0 +1,157 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build linux freebsd
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// isResolvconfActive indicates whether the system appears to be using resolvconf.
|
||||
// If this is true, then directManager should be avoided:
|
||||
// resolvconf has exclusive ownership of /etc/resolv.conf.
|
||||
func isResolvconfActive() bool {
|
||||
// Sanity-check first: if there is no resolvconf binary, then this is fruitless.
|
||||
//
|
||||
// However, this binary may be a shim like the one systemd-resolved provides.
|
||||
// Such a shim may not behave as expected: in particular, systemd-resolved
|
||||
// does not seem to respect the exclusive mode -x, saying:
|
||||
// -x Send DNS traffic preferably over this interface
|
||||
// whereas e.g. openresolv sends DNS traffix _exclusively_ over that interface,
|
||||
// or not at all (in case of another exclusive-mode request later in time).
|
||||
//
|
||||
// Moreover, resolvconf may be installed but unused, in which case we should
|
||||
// not use it either, lest we clobber existing configuration.
|
||||
//
|
||||
// To handle all the above correctly, we scan the comments in /etc/resolv.conf
|
||||
// to ensure that it was generated by a resolvconf implementation.
|
||||
_, err := exec.LookPath("resolvconf")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
f, err := os.Open("/etc/resolv.conf")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Bytes()
|
||||
// Look for the word "resolvconf" until comments end.
|
||||
if len(line) > 0 && line[0] != '#' {
|
||||
return false
|
||||
}
|
||||
if bytes.Contains(line, []byte("resolvconf")) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// resolvconfImpl enumerates supported implementations of the resolvconf CLI.
|
||||
type resolvconfImpl uint8
|
||||
|
||||
const (
|
||||
// resolvconfOpenresolv is the implementation packaged as "openresolv" on Ubuntu.
|
||||
// It supports exclusive mode and interface metrics.
|
||||
resolvconfOpenresolv resolvconfImpl = iota
|
||||
// resolvconfLegacy is the implementation by Thomas Hood packaged as "resolvconf" on Ubuntu.
|
||||
// It does not support exclusive mode or interface metrics.
|
||||
resolvconfLegacy
|
||||
)
|
||||
|
||||
func (impl resolvconfImpl) String() string {
|
||||
switch impl {
|
||||
case resolvconfOpenresolv:
|
||||
return "openresolv"
|
||||
case resolvconfLegacy:
|
||||
return "legacy"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// getResolvconfImpl returns the implementation of resolvconf that appears to be in use.
|
||||
func getResolvconfImpl() resolvconfImpl {
|
||||
err := exec.Command("resolvconf", "-v").Run()
|
||||
if err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
// Thomas Hood's resolvconf has a minimal flag set
|
||||
// and exits with code 99 when passed an unknown flag.
|
||||
if exitErr.ExitCode() == 99 {
|
||||
return resolvconfLegacy
|
||||
}
|
||||
}
|
||||
}
|
||||
return resolvconfOpenresolv
|
||||
}
|
||||
|
||||
type resolvconfManager struct {
|
||||
impl resolvconfImpl
|
||||
}
|
||||
|
||||
func newResolvconfManager(mconfig ManagerConfig) managerImpl {
|
||||
impl := getResolvconfImpl()
|
||||
mconfig.Logf("resolvconf implementation is %s", impl)
|
||||
|
||||
return resolvconfManager{
|
||||
impl: impl,
|
||||
}
|
||||
}
|
||||
|
||||
// resolvconfConfigName is the name of the config submitted to resolvconf.
|
||||
// It has this form to match the "tun*" rule in interface-order
|
||||
// when running resolvconfLegacy, hopefully placing our config first.
|
||||
const resolvconfConfigName = "tun-tailscale.inet"
|
||||
|
||||
// Up implements managerImpl.
|
||||
func (m resolvconfManager) Up(config Config) error {
|
||||
stdin := new(bytes.Buffer)
|
||||
writeResolvConf(stdin, config.Nameservers, config.Domains) // dns_direct.go
|
||||
|
||||
var cmd *exec.Cmd
|
||||
switch m.impl {
|
||||
case resolvconfOpenresolv:
|
||||
// Request maximal priority (metric 0) and exclusive mode.
|
||||
cmd = exec.Command("resolvconf", "-m", "0", "-x", "-a", resolvconfConfigName)
|
||||
case resolvconfLegacy:
|
||||
// This does not quite give us the desired behavior (queries leak),
|
||||
// but there is nothing else we can do without messing with other interfaces' settings.
|
||||
cmd = exec.Command("resolvconf", "-a", resolvconfConfigName)
|
||||
}
|
||||
cmd.Stdin = stdin
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("running %s: %s", cmd, out)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Down implements managerImpl.
|
||||
func (m resolvconfManager) Down() error {
|
||||
var cmd *exec.Cmd
|
||||
switch m.impl {
|
||||
case resolvconfOpenresolv:
|
||||
cmd = exec.Command("resolvconf", "-f", "-d", resolvconfConfigName)
|
||||
case resolvconfLegacy:
|
||||
// resolvconfLegacy lacks the -f flag.
|
||||
// Instead, it succeeds even when the config does not exist.
|
||||
cmd = exec.Command("resolvconf", "-d", resolvconfConfigName)
|
||||
}
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("running %s: %s", cmd, out)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
188
net/dns/resolved.go
Normal file
188
net/dns/resolved.go
Normal file
@@ -0,0 +1,188 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build linux
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
"golang.org/x/sys/unix"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/interfaces"
|
||||
)
|
||||
|
||||
// resolvedListenAddr is the listen address of the resolved stub resolver.
|
||||
//
|
||||
// We only consider resolved to be the system resolver if the stub resolver is;
|
||||
// that is, if this address is the sole nameserver in /etc/resolved.conf.
|
||||
// In other cases, resolved may be managing the system DNS configuration directly.
|
||||
// Then the nameserver list will be a concatenation of those for all
|
||||
// the interfaces that register their interest in being a default resolver with
|
||||
// SetLinkDomains([]{{"~.", true}, ...})
|
||||
// which includes at least the interface with the default route, i.e. not us.
|
||||
// This does not work for us: there is a possibility of getting NXDOMAIN
|
||||
// from the other nameservers before we are asked or get a chance to respond.
|
||||
// We consider this case as lacking resolved support and fall through to dnsDirect.
|
||||
//
|
||||
// While it may seem that we need to read a config option to get at this,
|
||||
// this address is, in fact, hard-coded into resolved.
|
||||
var resolvedListenAddr = netaddr.IPv4(127, 0, 0, 53)
|
||||
|
||||
var errNotReady = errors.New("interface not ready")
|
||||
|
||||
type resolvedLinkNameserver struct {
|
||||
Family int32
|
||||
Address []byte
|
||||
}
|
||||
|
||||
type resolvedLinkDomain struct {
|
||||
Domain string
|
||||
RoutingOnly bool
|
||||
}
|
||||
|
||||
// isResolvedActive determines if resolved is currently managing system DNS settings.
|
||||
func isResolvedActive() bool {
|
||||
// systemd-resolved is never installed without systemd.
|
||||
_, err := exec.LookPath("systemctl")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// is-active exits with code 3 if the service is not active.
|
||||
err = exec.Command("systemctl", "is-active", "systemd-resolved").Run()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
config, err := readResolvConf()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// The sole nameserver must be the systemd-resolved stub.
|
||||
if len(config.Nameservers) == 1 && config.Nameservers[0] == resolvedListenAddr {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// resolvedManager uses the systemd-resolved DBus API.
|
||||
type resolvedManager struct{}
|
||||
|
||||
func newResolvedManager(mconfig ManagerConfig) managerImpl {
|
||||
return resolvedManager{}
|
||||
}
|
||||
|
||||
// Up implements managerImpl.
|
||||
func (m resolvedManager) Up(config Config) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), reconfigTimeout)
|
||||
defer cancel()
|
||||
|
||||
// conn is a shared connection whose lifecycle is managed by the dbus package.
|
||||
// We should not interfere with that by closing it.
|
||||
conn, err := dbus.SystemBus()
|
||||
if err != nil {
|
||||
return fmt.Errorf("connecting to system bus: %w", err)
|
||||
}
|
||||
|
||||
resolved := conn.Object(
|
||||
"org.freedesktop.resolve1",
|
||||
dbus.ObjectPath("/org/freedesktop/resolve1"),
|
||||
)
|
||||
|
||||
// In principle, we could persist this in the manager struct
|
||||
// if we knew that interface indices are persistent. This does not seem to be the case.
|
||||
_, iface, err := interfaces.Tailscale()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting interface index: %w", err)
|
||||
}
|
||||
if iface == nil {
|
||||
return errNotReady
|
||||
}
|
||||
|
||||
var linkNameservers = make([]resolvedLinkNameserver, len(config.Nameservers))
|
||||
for i, server := range config.Nameservers {
|
||||
ip := server.As16()
|
||||
if server.Is4() {
|
||||
linkNameservers[i] = resolvedLinkNameserver{
|
||||
Family: unix.AF_INET,
|
||||
Address: ip[12:],
|
||||
}
|
||||
} else {
|
||||
linkNameservers[i] = resolvedLinkNameserver{
|
||||
Family: unix.AF_INET6,
|
||||
Address: ip[:],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = resolved.CallWithContext(
|
||||
ctx, "org.freedesktop.resolve1.Manager.SetLinkDNS", 0,
|
||||
iface.Index, linkNameservers,
|
||||
).Store()
|
||||
if err != nil {
|
||||
return fmt.Errorf("setLinkDNS: %w", err)
|
||||
}
|
||||
|
||||
var linkDomains = make([]resolvedLinkDomain, len(config.Domains))
|
||||
for i, domain := range config.Domains {
|
||||
linkDomains[i] = resolvedLinkDomain{
|
||||
Domain: domain,
|
||||
RoutingOnly: false,
|
||||
}
|
||||
}
|
||||
|
||||
err = resolved.CallWithContext(
|
||||
ctx, "org.freedesktop.resolve1.Manager.SetLinkDomains", 0,
|
||||
iface.Index, linkDomains,
|
||||
).Store()
|
||||
if err != nil {
|
||||
return fmt.Errorf("setLinkDomains: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Down implements managerImpl.
|
||||
func (m resolvedManager) Down() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), reconfigTimeout)
|
||||
defer cancel()
|
||||
|
||||
// conn is a shared connection whose lifecycle is managed by the dbus package.
|
||||
// We should not interfere with that by closing it.
|
||||
conn, err := dbus.SystemBus()
|
||||
if err != nil {
|
||||
return fmt.Errorf("connecting to system bus: %w", err)
|
||||
}
|
||||
|
||||
resolved := conn.Object(
|
||||
"org.freedesktop.resolve1",
|
||||
dbus.ObjectPath("/org/freedesktop/resolve1"),
|
||||
)
|
||||
|
||||
_, iface, err := interfaces.Tailscale()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting interface index: %w", err)
|
||||
}
|
||||
if iface == nil {
|
||||
return errNotReady
|
||||
}
|
||||
|
||||
err = resolved.CallWithContext(
|
||||
ctx, "org.freedesktop.resolve1.Manager.RevertLink", 0,
|
||||
iface.Index,
|
||||
).Store()
|
||||
if err != nil {
|
||||
return fmt.Errorf("RevertLink: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
Reference in New Issue
Block a user