fix(cli): set the Intercept DNS target on IPv6-only networks before CLAT starts

On macOS in Intercept mode, an IPv6-only network that offers no DHCPv4
and no PREF64 ended in a DNS outage. The DHCP option-6 read fails there
because no lease exists, and the native-CLAT evidence does not exist yet
because macOS starts its CLAT only after RFC 7050 prefix discovery
(ipv4only.arpa) succeeds. pf blocks IPv6 port 53 and the service has no
IPv4 DNS, so that lookup can never be sent. ctrld waited for evidence
that only DNS could produce.

A failed DHCP read now admits the target with a second kind of positive
native evidence: configd publishes no global or per-service IPv4 state,
the IPv6 primary service is this device, and the interface holds only
IPv6 addresses (at least one routable). The service mapping, unique
enabled networksetup name, two-second shared budget, tuple recheck,
validated snapshot backup and ownership rules are the same as on the
CLAT path. Unknown or changing state still admits nothing. A readable
DHCP lease never consults the new evidence, and CLAT evidence keeps
precedence. The journal line records which evidence admitted the
target.
This commit is contained in:
Dev Scribe authored and Cuong Manh Le committed 2026-10-20 17:23:47 +07:00
1 parent b8616e4ec9
commit 50caf516bd
6 files changed
+430 -3

No files matched your search

+28 -1
View File
@@ -27,6 +27,7 @@ var (
interceptPatchNetIfaceNameFn = patchNetIfaceName
interceptCurrentStaticDNSFn = readTargetStaticDNS
interceptNativeCLATDefaultServiceFn = nativeCLATDefaultService
interceptNativeIPv6OnlyServiceFn = nativeIPv6OnlyDefaultService
interceptNativeStaticDNSFn = readNativeTargetStaticDNS
interceptSaveStaticDNSSnapshotFn = func(iface *net.Interface, dns []string, owned string) error {
return saveTargetStaticDNSSnapshot(savedStaticDnsSettingsFilePath(iface), dns, owned)
@@ -135,6 +136,13 @@ func (p *prog) persistInterceptDNSTargetStateLocked() error {
// IPv4 DNS and on intercept shutdown. Networks that provide IPv4 DNS are
// never modified.
//
// A failed DHCP option-6 read is not absence on its own. It admits a target
// only with positive native evidence that the network has no IPv4: either
// macOS's own CLAT on the primary service, or (before macOS has started a
// CLAT) no IPv4 state at all with the device as the IPv6 primary. Without a
// target there, macOS cannot emit the ipv4only.arpa lookup it needs to start
// the CLAT, so the CLAT evidence alone can never arrive (issue #629).
//
// Callers pass a non-nil raw system discovery result to prove discovery ran;
// an empty slice is a valid DNS-less result. The decision itself uses static
// DNS plus DHCP option 6 from the default-route interface, so resolvers on a
@@ -187,6 +195,8 @@ func (p *prog) ensureInterceptDNSTarget(systemDiscovery []string) {
ctx, cancel := context.WithTimeout(context.Background(), nativeTargetReadBudget)
defer cancel()
var nativeService nativeTargetService
nativeEvidence := ""
nativeServiceFn := interceptNativeCLATDefaultServiceFn
readStatic := interceptCurrentStaticDNSFn
if dhcpErr != nil {
// Resolve the exact primary UUID before any expanded mutation. A
@@ -196,6 +206,22 @@ func (p *prog) ensureInterceptDNSTarget(systemDiscovery []string) {
nativeFallback = nativeErr == nil && nativeService.ID != "" && nativeService.Name != "" && nativeService.Device == drIfaceName && !hasIPv4DNS(routeDHCPDNS)
mainLog.Load().Debug().Err(nativeErr).Bool("native_clat_verified", nativeFallback).
Str("interface", drIfaceName).Msg("intercept DNS target: native fallback evidence")
if nativeFallback {
nativeEvidence = "clat"
} else {
// Before macOS starts its CLAT, an IPv6-only network without
// DHCPv4 has neither a lease to read nor a CLAT to prove. Its
// absence of IPv4 is the evidence instead (issue #629).
var v6Err error
nativeService, v6Err = interceptNativeIPv6OnlyServiceFn(ctx, drIfaceName)
nativeFallback = v6Err == nil && nativeService.ID != "" && nativeService.Name != "" && nativeService.Device == drIfaceName && !hasIPv4DNS(routeDHCPDNS)
mainLog.Load().Debug().Err(v6Err).Bool("native_ipv6_only_verified", nativeFallback).
Str("interface", drIfaceName).Msg("intercept DNS target: native fallback evidence")
if nativeFallback {
nativeEvidence = "ipv6_only"
nativeServiceFn = interceptNativeIPv6OnlyServiceFn
}
}
}
if nativeFallback {
iface.Name = nativeService.Name
@@ -218,7 +244,7 @@ func (p *prog) ensureInterceptDNSTarget(systemDiscovery []string) {
snapshot := staticDNS
if nativeFallback {
device, routeErr := interceptDefaultRouteInterfaceFn()
check, serviceErr := interceptNativeCLATDefaultServiceFn(ctx, drIfaceName)
check, serviceErr := nativeServiceFn(ctx, drIfaceName)
if routeErr != nil || serviceErr != nil || device != drIfaceName || check != nativeService {
diagnostic.failed(decision, "native_recheck", errors.Join(routeErr, serviceErr))
return
@@ -318,6 +344,7 @@ func (p *prog) ensureInterceptDNSTarget(systemDiscovery []string) {
}
journal(mainLog.Load().Warn()).Str("service", iface.Name).Str("target", target).
Bool("native_clat_fallback", nativeFallback).
Str("native_evidence", nativeEvidence).
Str("reason", "dns_less_network").
Msgf("intercept DNS target: service %q provides no usable IPv4 DNS; set %s so macOS can emit DNS queries (removed automatically when the network provides IPv4 DNS)", iface.Name, target)
}
+12
View File
@@ -23,6 +23,9 @@ type interceptTargetHarness struct {
dhcpErr error
nativeCLAT bool
nativeErr error
ipv6Only bool
ipv6OnlyErr error
ipv6Calls int
saveErr error
readErr error
setErr error
@@ -55,6 +58,7 @@ func newInterceptTargetHarness(t *testing.T) *interceptTargetHarness {
origReset := interceptResetDNSIgnoreUnusableIfaceFn
origDHCP := interceptDHCPNameserversForInterfaceFn
origNative := interceptNativeCLATDefaultServiceFn
origIPv6Only := interceptNativeIPv6OnlyServiceFn
origNativeStatic := interceptNativeStaticDNSFn
origSnapshot := interceptSaveStaticDNSSnapshotFn
origIntercept := dnsIntercept
@@ -70,6 +74,7 @@ func newInterceptTargetHarness(t *testing.T) *interceptTargetHarness {
interceptResetDNSIgnoreUnusableIfaceFn = origReset
interceptDHCPNameserversForInterfaceFn = origDHCP
interceptNativeCLATDefaultServiceFn = origNative
interceptNativeIPv6OnlyServiceFn = origIPv6Only
interceptNativeStaticDNSFn = origNativeStatic
interceptSaveStaticDNSSnapshotFn = origSnapshot
dnsIntercept = origIntercept
@@ -81,6 +86,13 @@ func newInterceptTargetHarness(t *testing.T) *interceptTargetHarness {
}
return nativeTargetService{ID: nativeTargetTestServiceID, Name: h.serviceByDev[device], Device: device}, nil
}
interceptNativeIPv6OnlyServiceFn = func(_ context.Context, device string) (nativeTargetService, error) {
h.ipv6Calls++
if !h.ipv6Only || h.ipv6OnlyErr != nil {
return nativeTargetService{}, h.ipv6OnlyErr
}
return nativeTargetService{ID: nativeTargetTestServiceID, Name: h.serviceByDev[device], Device: device}, nil
}
interceptNativeStaticDNSFn = func(_ context.Context, iface *net.Interface) ([]string, error) {
return interceptCurrentStaticDNSFn(iface)
}
+103
View File
@@ -0,0 +1,103 @@
//go:build darwin
package cli
import (
"context"
"errors"
"slices"
"testing"
)
// Issue #629: an IPv6-only network without DHCPv4 and before macOS starts
// its CLAT has neither a lease to read nor a CLAT to prove.
func TestIPv6OnlyTargetAdmission(t *testing.T) {
for _, tc := range []struct {
name string
ipv6Only, clat bool
ipv6Err error
static, dhcp []string
dhcpErr error
want bool
wantIPv6Checked bool
}{
{name: "no IPv4 at all", ipv6Only: true, dhcpErr: errors.New("getoption: exit status 1; getpacket: exit status 1"), want: true, wantIPv6Checked: true},
{name: "IPv6 static DNS kept for restore", ipv6Only: true, static: []string{"2001:db8::53"}, dhcpErr: errors.New("no lease"), want: true, wantIPv6Checked: true},
{name: "unknown native state", ipv6Err: errors.New("scutil failed"), dhcpErr: errors.New("no lease"), wantIPv6Checked: true},
{name: "IPv4 present", dhcpErr: errors.New("no lease"), wantIPv6Checked: true},
{name: "static IPv4 wins", ipv6Only: true, static: []string{"9.9.9.9"}, dhcpErr: errors.New("no lease"), wantIPv6Checked: true},
{name: "readable DHCP never consults IPv6-only evidence", ipv6Only: true, dhcp: []string{"192.168.1.1"}},
{name: "CLAT evidence takes precedence", clat: true, ipv6Only: true, dhcpErr: errors.New("no lease"), want: true},
} {
t.Run(tc.name, func(t *testing.T) {
h := newInterceptTargetHarness(t)
h.ipv6Only, h.ipv6OnlyErr, h.nativeCLAT = tc.ipv6Only, tc.ipv6Err, tc.clat
h.dns["Wi-Fi"], h.dhcp, h.dhcpErr = slices.Clone(tc.static), tc.dhcp, tc.dhcpErr
p := newInterceptTargetProg()
p.ensureInterceptDNSTarget([]string{})
if got := len(h.setCalls) > 0; got != tc.want || len(h.resetCalls) != 0 {
t.Fatalf("set=%v reset=%v, want set=%v", h.setCalls, h.resetCalls, tc.want)
}
if (h.ipv6Calls > 0) != tc.wantIPv6Checked {
t.Fatalf("IPv6-only evidence consulted %d times, want consulted=%v", h.ipv6Calls, tc.wantIPv6Checked)
}
if tc.want && !slices.Equal(h.dns["Wi-Fi"], []string{"127.0.0.53"}) {
t.Fatalf("target = %v, want [127.0.0.53]", h.dns["Wi-Fi"])
}
})
}
}
// The admitted target is removed when the network gains IPv4, and the
// original IPv6 static DNS comes back.
func TestIPv6OnlyTargetLifecycle(t *testing.T) {
h := newInterceptTargetHarness(t)
h.ipv6Only = true
h.dhcpErr = errors.New("no lease")
h.dns["Wi-Fi"] = []string{"2001:db8::53"}
p := newInterceptTargetProg()
p.ensureInterceptDNSTarget([]string{})
if p.interceptDNSTargetService != "Wi-Fi" {
t.Fatal("no target on IPv6-only network")
}
// Repeated reconciliation is idempotent.
p.ensureInterceptDNSTarget([]string{})
if len(h.setCalls) != 1 {
t.Fatalf("target churn: %v", h.setCalls)
}
// Unknown native state later never removes or rewrites ownership.
h.ipv6Only, h.ipv6OnlyErr = false, errors.New("store unavailable")
p.ensureInterceptDNSTarget([]string{})
if p.interceptDNSTargetService != "Wi-Fi" || len(h.setCalls) != 1 || len(h.resetCalls) != 0 {
t.Fatal("unknown state changed ownership")
}
// Back on a DHCPv4 network: the target goes, the user's IPv6 DNS returns.
h.ipv6OnlyErr, h.dhcpErr, h.dhcp = nil, nil, []string{"192.168.1.1"}
p.ensureInterceptDNSTarget([]string{})
if p.interceptDNSTargetService != "" || !slices.Equal(h.dns["Wi-Fi"], []string{"2001:db8::53"}) {
t.Fatalf("target not removed on DHCPv4 return: owner=%q dns=%v", p.interceptDNSTargetService, h.dns["Wi-Fi"])
}
}
// The IPv6-only path rechecks with its own reader, not the CLAT reader, and a
// changed tuple during the transaction blocks the write.
func TestIPv6OnlyTargetRecheck(t *testing.T) {
h := newInterceptTargetHarness(t)
h.ipv6Only = true
h.dhcpErr = errors.New("no lease")
read := interceptNativeIPv6OnlyServiceFn
calls := 0
interceptNativeIPv6OnlyServiceFn = func(ctx context.Context, device string) (nativeTargetService, error) {
calls++
svc, err := read(ctx, device)
if calls == 2 {
svc.ID = nativeTargetOtherServiceID
}
return svc, err
}
p := newInterceptTargetProg()
p.ensureInterceptDNSTarget([]string{})
if calls != 2 || len(h.setCalls) != 0 || p.interceptDNSTargetService != "" {
t.Fatalf("changed IPv6-only tuple admitted a target: calls=%d set=%v", calls, h.setCalls)
}
}
+134
View File
@@ -0,0 +1,134 @@
package cli
import (
"context"
"errors"
"net"
"slices"
"strings"
"testing"
)
// Captured on the ctrld-qa macOS 26 guest on an IPv6-only network without
// DHCPv4 or PREF64, before the CLAT started (ctrld#629).
const (
ipv6OnlyTestGlobal6 = "<dictionary> {\n PrimaryInterface : en1\n PrimaryService : " + nativeTargetTestServiceID + "\n Router : fe80::5054:ff:fe51:3\n}"
ipv6OnlyTestNoKey = " No such key"
)
func ipv6OnlyTestOutputs() []string {
return []string{
ipv6OnlyTestNoKey, // State:/Network/Global/IPv4
ipv6OnlyTestGlobal6, // State:/Network/Global/IPv6
ipv6OnlyTestNoKey, // State:/Network/Service/<id>/IPv4
"<dictionary> {\n UserDefinedName : Primary Wi-Fi\n}",
nativeTargetTestServiceOrder("Primary Wi-Fi", "en1"),
ipv6OnlyTestNoKey, // recheck Global/IPv4
ipv6OnlyTestGlobal6, // recheck Global/IPv6
}
}
func ipv6OnlyTestReader(t *testing.T, outputs []string, calls *[]string, addrs ...string) nativeTargetReader {
t.Helper()
r := nativeTargetTestReader(t, outputs, calls)
if len(addrs) == 0 {
addrs = []string{"fd66:6:6:0:1876:fb78:b312:107b/64", "fe80::440:5b5a:3e67:23fe/64"}
}
r.interfaceAddrs = func(*net.Interface) ([]net.Addr, error) { return nativeTargetTestAddrs(addrs...), nil }
return r
}
func TestNativeIPv6OnlyReader(t *testing.T) {
for _, tc := range []struct {
name string
change func(*nativeTargetReader, []string)
addrs []string
want bool
}{
{name: "captured IPv6-only state", want: true},
{name: "global IPv4 present", change: func(_ *nativeTargetReader, out []string) {
out[0] = nativeTargetTestGlobal(nativeTargetTestServiceID, "en1")
}},
{name: "service IPv4 present", change: func(_ *nativeTargetReader, out []string) {
out[2] = nativeTargetTestState()
}},
{name: "IPv6 primary is another device", change: func(_ *nativeTargetReader, out []string) {
out[1] = strings.Replace(ipv6OnlyTestGlobal6, "en1", "en9", 1)
}},
{name: "CLAT address on the interface", addrs: []string{"192.0.0.2/32", "2001:db8::1/64"}},
{name: "any IPv4 address on the interface", addrs: []string{"169.254.10.10/16", "2001:db8::1/64"}},
{name: "link-local IPv6 only", addrs: []string{"fe80::1/64"}},
{name: "unknown store response", change: func(_ *nativeTargetReader, out []string) {
out[0] = "scutil: permission denied"
}},
{name: "unknown IPv6 field", change: func(_ *nativeTargetReader, out []string) {
out[1] = strings.Replace(ipv6OnlyTestGlobal6, "}", " Surprise : 1\n}", 1)
}},
{name: "IPv4 appears before recheck", change: func(_ *nativeTargetReader, out []string) {
out[5] = nativeTargetTestGlobal(nativeTargetTestServiceID, "en1")
}},
{name: "primary changes before recheck", change: func(_ *nativeTargetReader, out []string) {
out[6] = strings.Replace(ipv6OnlyTestGlobal6, nativeTargetTestServiceID, nativeTargetOtherServiceID, 1)
}},
{name: "ambiguous service mapping", change: func(_ *nativeTargetReader, out []string) {
out[4] += "(2) Secondary Wi-Fi\n(Hardware Port: Wi-Fi, Device: en1)\n"
}},
} {
t.Run(tc.name, func(t *testing.T) {
var calls []string
out := ipv6OnlyTestOutputs()
r := ipv6OnlyTestReader(t, out, &calls, tc.addrs...)
if tc.change != nil {
tc.change(&r, out)
}
svc, err := r.ipv6OnlyService(context.Background(), "en1")
got := err == nil && svc.ID == nativeTargetTestServiceID && svc.Name == "Primary Wi-Fi" && svc.Device == "en1"
if got != tc.want {
t.Fatalf("service=%+v err=%v, want admitted=%v", svc, err, tc.want)
}
if !tc.want && svc != (nativeTargetService{}) {
t.Fatalf("rejected state returned a service: %+v", svc)
}
})
}
}
func TestNativeIPv6OnlyReaderHonorsBudget(t *testing.T) {
var calls []string
r := ipv6OnlyTestReader(t, ipv6OnlyTestOutputs(), &calls)
ctx, cancel := context.WithCancel(context.Background())
cancel()
if svc, err := r.ipv6OnlyService(ctx, "en1"); !errors.Is(err, context.Canceled) || svc != (nativeTargetService{}) || len(calls) != 0 {
t.Fatalf("expired budget read state: svc=%+v err=%v calls=%v", svc, err, calls)
}
}
func TestNativeStoreAbsent(t *testing.T) {
for out, want := range map[string]bool{" No such key\n": true, "<dictionary> {\n}": false} {
if got, err := nativeStoreAbsent(out); err != nil || got != want {
t.Fatalf("%q: got %v %v", out, got, err)
}
}
for _, out := range []string{"", "No such key here", "error"} {
if _, err := nativeStoreAbsent(out); err == nil {
t.Fatalf("%q accepted as known", out)
}
}
}
func TestParseNativeIPv6GlobalRejectsUnsafe(t *testing.T) {
for _, out := range []string{
strings.Replace(ipv6OnlyTestGlobal6, "fe80::5054:ff:fe51:3", "192.168.1.1", 1),
strings.Replace(ipv6OnlyTestGlobal6, nativeTargetTestServiceID, "not-a-uuid", 1),
strings.Replace(ipv6OnlyTestGlobal6, "PrimaryInterface : en1", "PrimaryInterface : en1 en2", 1),
strings.Replace(ipv6OnlyTestGlobal6, "}", " PrimaryInterface : en1\n}", 1),
"<dictionary> {\n Router : fe80::1\n}",
} {
if _, _, err := parseNativeIPv6Global(out); err == nil {
t.Fatalf("accepted %q", out)
}
}
if !slices.Equal([]bool{nativeIPv6OnlyAddresses(nativeTargetTestAddrs("2001:db8::1/64"))}, []bool{true}) {
t.Fatal("global IPv6 rejected")
}
}
+143
View File
@@ -323,3 +323,146 @@ func nativeCLATAddresses(addrs []net.Addr) bool {
}
return clat && ipv6
}
// nativeStoreAbsent reports whether scutil printed its exact missing-key
// response. Absence is evidence only in that exact form; a dictionary means
// present, and anything else is unknown.
func nativeStoreAbsent(out string) (bool, error) {
s := strings.TrimSpace(out)
if s == "No such key" {
return true, nil
}
if strings.HasPrefix(s, "<dictionary> {") {
return false, nil
}
return false, errors.New("unknown native store response")
}
func (r nativeTargetReader) absent(ctx context.Context, key string) (bool, error) {
if err := ctx.Err(); err != nil {
return false, err
}
out, err := r.run(ctx, "show "+key+"\nquit\n", "/usr/sbin/scutil")
if err != nil {
return false, err
}
return nativeStoreAbsent(string(out))
}
// parseNativeIPv6Global reads only State:/Network/Global/IPv6. Unknown,
// nested or duplicate fields fail closed, like the IPv4 reader.
func parseNativeIPv6Global(out string) (id, device string, err error) {
lines := strings.Split(strings.TrimSpace(out), "\n")
if len(lines) < 2 || lines[0] != "<dictionary> {" || lines[len(lines)-1] != "}" {
return "", "", errors.New("invalid native IPv6 dictionary")
}
seen := make(map[string]bool)
for _, line := range lines[1 : len(lines)-1] {
key, value, ok := strings.Cut(strings.TrimSpace(line), " : ")
if !ok || strings.HasSuffix(value, "{") {
return "", "", errors.New("invalid native IPv6 field")
}
if seen[key] {
return "", "", errors.New("duplicate native IPv6 field")
}
seen[key] = true
switch key {
case "PrimaryService":
if !nativeServiceID.MatchString(value) {
return "", "", errors.New("invalid service ID")
}
id = value
case "PrimaryInterface":
if value == "" || strings.ContainsAny(value, " \r\n{}") {
return "", "", errors.New("invalid interface")
}
device = value
case "Router":
if ip := net.ParseIP(value); ip == nil || ip.To4() != nil {
return "", "", errors.New("invalid IPv6 router")
}
default:
return "", "", errors.New("unknown native IPv6 field")
}
}
if id == "" || device == "" {
return "", "", errors.New("incomplete native IPv6 dictionary")
}
return id, device, nil
}
// nativeIPv6OnlyAddresses requires routable IPv6 and no IPv4 address at all.
func nativeIPv6OnlyAddresses(addrs []net.Addr) bool {
ipv6 := false
for _, addr := range addrs {
ip, _, err := net.ParseCIDR(addr.String())
if err != nil || ip.To4() != nil {
return false
}
if ip.IsGlobalUnicast() {
ipv6 = true
}
}
return ipv6
}
// ipv6OnlyService returns the primary service of a network that gives the
// device no IPv4 at all: configd publishes no global or per-service IPv4
// state, the IPv6 primary is this device, and the interface holds only IPv6.
// Before macOS starts its CLAT, such a network has no DHCPv4 lease to read
// and no CLAT to prove, yet it is just as DNS-less (issue #629). Unknown or
// changing state is never absence evidence; the caller rechecks the tuple.
func (r nativeTargetReader) ipv6OnlyService(ctx context.Context, device string) (nativeTargetService, error) {
unknown := nativeTargetService{}
if gone, err := r.absent(ctx, "State:/Network/Global/IPv4"); err != nil || !gone {
return unknown, err
}
out, err := r.run(ctx, "show State:/Network/Global/IPv6\nquit\n", "/usr/sbin/scutil")
if err != nil {
return unknown, err
}
id, primary, err := parseNativeIPv6Global(string(out))
if err != nil || primary != device {
return unknown, err
}
if gone, err := r.absent(ctx, "State:/Network/Service/"+id+"/IPv4"); err != nil || !gone {
return unknown, err
}
iface, err := r.interfaceByName(device)
if err != nil {
return unknown, err
}
if iface == nil || iface.Name != device || iface.Flags&net.FlagUp == 0 {
return unknown, nil
}
addrs, err := r.interfaceAddrs(iface)
if err != nil || !nativeIPv6OnlyAddresses(addrs) {
return unknown, err
}
out, err = r.run(ctx, "show Setup:/Network/Service/"+id+"\nquit\n", "/usr/sbin/scutil")
if err != nil {
return unknown, err
}
if _, err = parseNativeServiceName(string(out)); err != nil {
return unknown, err
}
out, err = r.run(ctx, "", "/usr/sbin/networksetup", "-listnetworkserviceorder")
if err != nil {
return unknown, err
}
name, err := targetNetworkServiceName(string(out), device)
if err != nil {
return unknown, err
}
if gone, err := r.absent(ctx, "State:/Network/Global/IPv4"); err != nil || !gone {
return unknown, errors.Join(err, errors.New("IPv4 appeared during IPv6-only discovery"))
}
out, err = r.run(ctx, "show State:/Network/Global/IPv6\nquit\n", "/usr/sbin/scutil")
if err != nil {
return unknown, err
}
if afterID, afterDevice, err := parseNativeIPv6Global(string(out)); err != nil || afterID != id || afterDevice != device {
return unknown, errors.Join(err, errors.New("default service changed during IPv6-only discovery"))
}
return nativeTargetService{ID: id, Name: name, Device: device}, nil
}
+10 -2
View File
@@ -33,9 +33,17 @@ func readNativeTargetStaticDNS(ctx context.Context, iface *net.Interface) ([]str
}
func nativeCLATDefaultService(ctx context.Context, device string) (nativeTargetService, error) {
return (nativeTargetReader{
return newNativeTargetReader().defaultService(ctx, device)
}
func nativeIPv6OnlyDefaultService(ctx context.Context, device string) (nativeTargetService, error) {
return newNativeTargetReader().ipv6OnlyService(ctx, device)
}
func newNativeTargetReader() nativeTargetReader {
return nativeTargetReader{
run: runNativeTargetCommand,
interfaceByName: net.InterfaceByName,
interfaceAddrs: (*net.Interface).Addrs,
}).defaultService(ctx, device)
}
}