mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-10-24 14:08:27 +02:00
When a discovered VPN resolver is Tailscale's quad-100, read the Split DNS and App Connector routes from Tailscale's LocalAPI and route those suffixes to quad-100 instead of Control D. Master adaptation: newVPNDNSManager keeps master's logger argument and Refresh keeps its context argument. tailscaleRoutesFor takes the refresh context, and a retry builds its context from the manager's logger (backgroundCtx). setDNS closes the previous manager and wires the gated retry the same way as on v1.0.
116 lines
4.4 KiB
Go
116 lines
4.4 KiB
Go
//go:build darwin
|
|
|
|
package cli
|
|
|
|
import (
|
|
"context"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// Locations used by the three macOS Tailscale variants. These match
|
|
// tailscale.com/safesocket and tailscale.com/paths.
|
|
const (
|
|
tailscaleMacsysDir = "/Library/Tailscale"
|
|
tailscaledUnixSocket = "/var/run/tailscaled.socket"
|
|
)
|
|
|
|
// tailscaleCodeRequirement accepts only the Mac App Store Tailscale network
|
|
// extension. Apple re-signs App Store code with its own leaf certificate
|
|
// (marker OID 1.2.840.113635.100.6.1.9) and signs a bundle identifier only for
|
|
// the developer that owns it; other builds carry Tailscale's team identifier
|
|
// W5364U7YZB, which Tailscale publishes for its macOS extension policy.
|
|
const tailscaleCodeRequirement = `anchor apple generic and identifier "io.tailscale.ipn.macos.network-extension" and ` +
|
|
`(certificate leaf[field.1.2.840.113635.100.6.1.9] or certificate leaf[subject.OU] = "W5364U7YZB")`
|
|
|
|
// discoverTailscaleRoutesFn reads Tailscale DNS routes; tests replace it.
|
|
//
|
|
// discoverTailscaleRoutes reads Tailscale's DNS routes through its LocalAPI.
|
|
// ctrld runs as root, so it can read the credentials of each variant. The
|
|
// root-owned credentials come first: the Standalone system extension
|
|
// (/Library/Tailscale) and open source tailscaled (unix socket). The Mac App
|
|
// Store extension (sameuserproof file found with lsof) comes last, and only
|
|
// after its process passes a Tailscale code signature check, because any
|
|
// local user can start a process with that name.
|
|
var discoverTailscaleRoutesFn = discoverTailscaleRoutes
|
|
|
|
// listAppStoreProofsFn lists the open files of IPNExtension processes; tests
|
|
// replace it. "+c 0" prints the full command name, so the parser can require
|
|
// an exact match; -c alone matches any command that starts with the name.
|
|
var listAppStoreProofsFn = func(ctx context.Context) ([]byte, error) {
|
|
return exec.CommandContext(ctx, "lsof", "-n", "-a", "+c", "0", "-c", tailscaleAppStoreProc, "-F", "pcn").Output()
|
|
}
|
|
|
|
// verifyTailscaleProcessFn checks the code signature of a running process;
|
|
// tests replace it.
|
|
var verifyTailscaleProcessFn = verifyTailscaleProcess
|
|
|
|
func discoverTailscaleRoutes(ctx context.Context) ([]tailscaleRoute, error) {
|
|
return discoverTailscaleRoutesFrom(ctx, tailscaleLocalAPIEndpoints(ctx))
|
|
}
|
|
|
|
func tailscaleLocalAPIEndpoints(ctx context.Context) []tailscaleLocalAPIEndpoint {
|
|
var eps []tailscaleLocalAPIEndpoint
|
|
|
|
if portStr, err := os.Readlink(filepath.Join(tailscaleMacsysDir, "ipnport")); err == nil {
|
|
if port, err := strconv.Atoi(portStr); err == nil && port > 0 && port <= 65535 {
|
|
if b, err := os.ReadFile(filepath.Join(tailscaleMacsysDir, "sameuserproof-"+portStr)); err == nil {
|
|
if token := strings.TrimSpace(string(b)); token != "" {
|
|
eps = append(eps, tailscaleLocalAPIEndpoint{name: "standalone", dial: tcpLocalAPIDialer(port), token: token})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if fi, err := os.Stat(tailscaledUnixSocket); err == nil && fi.Mode()&os.ModeSocket != 0 {
|
|
eps = append(eps, tailscaleLocalAPIEndpoint{
|
|
name: "tailscaled",
|
|
dial: func(ctx context.Context) (net.Conn, error) {
|
|
var d net.Dialer
|
|
return d.DialContext(ctx, "unix", tailscaledUnixSocket)
|
|
},
|
|
})
|
|
}
|
|
|
|
return append(eps, appStoreLocalAPIEndpoints(ctx)...)
|
|
}
|
|
|
|
// appStoreLocalAPIEndpoints returns the App Store LocalAPI endpoint of the
|
|
// first IPNExtension process that is signed by Tailscale.
|
|
func appStoreLocalAPIEndpoints(ctx context.Context) []tailscaleLocalAPIEndpoint {
|
|
out, err := listAppStoreProofsFn(ctx)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
verified := make(map[int]bool)
|
|
for _, proof := range parseMacOSSameUserProofs(out) {
|
|
ok, seen := verified[proof.pid]
|
|
if !seen {
|
|
ok = verifyTailscaleProcessFn(ctx, proof.pid)
|
|
verified[proof.pid] = ok
|
|
}
|
|
if ok {
|
|
return []tailscaleLocalAPIEndpoint{{name: "app-store", dial: tcpLocalAPIDialer(proof.port), token: proof.token}}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// verifyTailscaleProcess reports whether the running process pid is signed by
|
|
// Tailscale. codesign validates a running process dynamically when given a
|
|
// pid instead of a path.
|
|
func verifyTailscaleProcess(ctx context.Context, pid int) bool {
|
|
return exec.CommandContext(ctx, "codesign", "--verify", "-R="+tailscaleCodeRequirement, strconv.Itoa(pid)).Run() == nil
|
|
}
|
|
|
|
func tcpLocalAPIDialer(port int) func(ctx context.Context) (net.Conn, error) {
|
|
return func(ctx context.Context) (net.Conn, error) {
|
|
var d net.Dialer
|
|
return d.DialContext(ctx, "tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(port)))
|
|
}
|
|
}
|