mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-02-03 22:18:39 +00:00
On Ubuntu 18.04 VM with some cloud provider, using dbus call to set DNS is forbidden. A possible solution is stopping networkd entirely then using systemd-resolve to set DNS when ctrld starts. While at it, only set DNS during start command on Windows. On other platforms, "ctrld run" does set DNS in service mode already. When using systemd-resolved, only change listener address to default route interface address if a loopback address is used. Also fixing a bug in upstream tailscale code for checking in container. See tailscale/tailscale#8444
258 lines
6.2 KiB
Go
258 lines
6.2 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/netip"
|
|
"os/exec"
|
|
"reflect"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/insomniacslk/dhcp/dhcpv4/nclient4"
|
|
"github.com/insomniacslk/dhcp/dhcpv6"
|
|
"github.com/insomniacslk/dhcp/dhcpv6/client6"
|
|
"tailscale.com/util/dnsname"
|
|
|
|
"github.com/Control-D-Inc/ctrld/internal/dns"
|
|
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
|
|
"github.com/Control-D-Inc/ctrld/internal/resolvconffile"
|
|
)
|
|
|
|
// allocate loopback ip
|
|
// sudo ip a add 127.0.0.2/24 dev lo
|
|
func allocateIP(ip string) error {
|
|
cmd := exec.Command("ip", "a", "add", ip+"/24", "dev", "lo")
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
mainLog.Error().Err(err).Msgf("allocateIP failed: %s", string(out))
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func deAllocateIP(ip string) error {
|
|
cmd := exec.Command("ip", "a", "del", ip+"/24", "dev", "lo")
|
|
if err := cmd.Run(); err != nil {
|
|
mainLog.Error().Err(err).Msg("deAllocateIP failed")
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
const maxSetDNSAttempts = 5
|
|
|
|
// set the dns server for the provided network interface
|
|
func setDNS(iface *net.Interface, nameservers []string) error {
|
|
r, err := dns.NewOSConfigurator(logf, iface.Name)
|
|
if err != nil {
|
|
mainLog.Error().Err(err).Msg("failed to create DNS OS configurator")
|
|
return err
|
|
}
|
|
|
|
ns := make([]netip.Addr, 0, len(nameservers))
|
|
for _, nameserver := range nameservers {
|
|
ns = append(ns, netip.MustParseAddr(nameserver))
|
|
}
|
|
|
|
osConfig := dns.OSConfig{
|
|
Nameservers: ns,
|
|
SearchDomains: []dnsname.FQDN{},
|
|
}
|
|
|
|
trySystemdResolve := false
|
|
for i := 0; i < maxSetDNSAttempts; i++ {
|
|
if err := r.SetDNS(osConfig); err != nil {
|
|
if strings.Contains(err.Error(), "Rejected send message") &&
|
|
strings.Contains(err.Error(), "org.freedesktop.network1.Manager") {
|
|
mainLog.Warn().Msg("Interfaces are managed by systemd-networkd, switch to systemd-resolve for setting DNS")
|
|
trySystemdResolve = true
|
|
break
|
|
}
|
|
return err
|
|
}
|
|
currentNS := currentDNS(iface)
|
|
if reflect.DeepEqual(currentNS, nameservers) {
|
|
return nil
|
|
}
|
|
}
|
|
if trySystemdResolve {
|
|
// Stop systemd-networkd and retry setting DNS.
|
|
if out, err := exec.Command("systemctl", "stop", "systemd-networkd").CombinedOutput(); err != nil {
|
|
return fmt.Errorf("%s: %w", string(out), err)
|
|
}
|
|
args := []string{"--interface=" + iface.Name, "--set-domain=~"}
|
|
for _, nameserver := range nameservers {
|
|
args = append(args, "--set-dns="+nameserver)
|
|
}
|
|
for i := 0; i < maxSetDNSAttempts; i++ {
|
|
if out, err := exec.Command("systemd-resolve", args...).CombinedOutput(); err != nil {
|
|
return fmt.Errorf("%s: %w", string(out), err)
|
|
}
|
|
currentNS := currentDNS(iface)
|
|
if reflect.DeepEqual(currentNS, nameservers) {
|
|
return nil
|
|
}
|
|
time.Sleep(time.Second)
|
|
}
|
|
}
|
|
mainLog.Debug().Msg("DNS was not set for some reason")
|
|
return nil
|
|
}
|
|
|
|
func resetDNS(iface *net.Interface) (err error) {
|
|
defer func() {
|
|
if err == nil {
|
|
return
|
|
}
|
|
// Start systemd-networkd if present.
|
|
if exe, _ := exec.LookPath("/lib/systemd/systemd-networkd"); exe != "" {
|
|
_ = exec.Command("systemctl", "restart", "systemd-networkd").Run()
|
|
}
|
|
if r, oerr := dns.NewOSConfigurator(logf, iface.Name); oerr == nil {
|
|
_ = r.SetDNS(dns.OSConfig{})
|
|
if err := r.Close(); err != nil {
|
|
mainLog.Error().Err(err).Msg("failed to rollback DNS setting")
|
|
return
|
|
}
|
|
err = nil
|
|
}
|
|
}()
|
|
|
|
var ns []string
|
|
c, err := nclient4.New(iface.Name)
|
|
if err != nil {
|
|
return fmt.Errorf("nclient4.New: %w", err)
|
|
}
|
|
defer c.Close()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
|
defer cancel()
|
|
lease, err := c.Request(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("nclient4.Request: %w", err)
|
|
}
|
|
for _, nameserver := range lease.ACK.DNS() {
|
|
if nameserver.Equal(net.IPv4zero) {
|
|
continue
|
|
}
|
|
ns = append(ns, nameserver.String())
|
|
}
|
|
|
|
// TODO(cuonglm): handle DHCPv6 properly.
|
|
if ctrldnet.IPv6Available(ctx) {
|
|
c := client6.NewClient()
|
|
conversation, err := c.Exchange(iface.Name)
|
|
if err != nil {
|
|
mainLog.Debug().Err(err).Msg("could not exchange DHCPv6")
|
|
}
|
|
for _, packet := range conversation {
|
|
if packet.Type() == dhcpv6.MessageTypeReply {
|
|
msg, err := packet.GetInnerMessage()
|
|
if err != nil {
|
|
mainLog.Debug().Err(err).Msg("could not get inner DHCPv6 message")
|
|
return nil
|
|
}
|
|
nameservers := msg.Options.DNS()
|
|
for _, nameserver := range nameservers {
|
|
ns = append(ns, nameserver.String())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return ignoringEINTR(func() error {
|
|
return setDNS(iface, ns)
|
|
})
|
|
}
|
|
|
|
func currentDNS(iface *net.Interface) []string {
|
|
for _, fn := range []getDNS{getDNSByResolvectl, getDNSBySystemdResolved, getDNSByNmcli, resolvconffile.NameServers} {
|
|
if ns := fn(iface.Name); len(ns) > 0 {
|
|
return ns
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func getDNSByResolvectl(iface string) []string {
|
|
b, err := exec.Command("resolvectl", "dns", "-i", iface).Output()
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
parts := strings.Fields(strings.SplitN(string(b), "%", 2)[0])
|
|
if len(parts) > 2 {
|
|
return parts[3:]
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func getDNSBySystemdResolved(iface string) []string {
|
|
b, err := exec.Command("systemd-resolve", "--status", iface).Output()
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return getDNSBySystemdResolvedFromReader(bytes.NewReader(b))
|
|
}
|
|
|
|
func getDNSBySystemdResolvedFromReader(r io.Reader) []string {
|
|
scanner := bufio.NewScanner(r)
|
|
var ret []string
|
|
for scanner.Scan() {
|
|
line := strings.TrimSpace(scanner.Text())
|
|
if len(ret) > 0 {
|
|
if net.ParseIP(line) != nil {
|
|
ret = append(ret, line)
|
|
}
|
|
continue
|
|
}
|
|
after, found := strings.CutPrefix(line, "DNS Servers: ")
|
|
if !found {
|
|
continue
|
|
}
|
|
if net.ParseIP(after) != nil {
|
|
ret = append(ret, after)
|
|
}
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func getDNSByNmcli(iface string) []string {
|
|
b, err := exec.Command("nmcli", "dev", "show", iface).Output()
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
s := bufio.NewScanner(bytes.NewReader(b))
|
|
var dns []string
|
|
do := func(line string) {
|
|
parts := strings.SplitN(line, ":", 2)
|
|
if len(parts) > 1 {
|
|
dns = append(dns, strings.TrimSpace(parts[1]))
|
|
}
|
|
}
|
|
for s.Scan() {
|
|
line := s.Text()
|
|
switch {
|
|
case strings.HasPrefix(line, "IP4.DNS"):
|
|
fallthrough
|
|
case strings.HasPrefix(line, "IP6.DNS"):
|
|
do(line)
|
|
}
|
|
}
|
|
return dns
|
|
}
|
|
|
|
func ignoringEINTR(fn func() error) error {
|
|
for {
|
|
err := fn()
|
|
if err != syscall.EINTR {
|
|
return err
|
|
}
|
|
}
|
|
}
|