diff --git a/README.md b/README.md index 35f08f0..e04b7f1 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ - **双向转发**:`forward`(Tailscale → 本地)与 `connect`(本地 → Tailscale)两种模式 - **TCP / UDP 全支持**:透明转发 TCP 流与 UDP 数据包 - **Minecraft 专用模式**:支持局域网广播发现(MOTD),让本地设备发现 Tailnet 上的 Minecraft 服务器 -- **MagicDNS 主机名补全**:`dst_addr` 支持短主机名(如 `home:8080`),启动时自动补全为 `home.:8080` +- **MagicDNS 主机名补全**:`dst_addr` 支持按照 Tailscale 规则正确解析 Split DNS 和 Magic DNS - **连接类型识别**:区分 `direct` 直连与 `derp` 中继,便于排查延迟问题 - **对端连通性诊断**:定期 ping 目标节点并报告延迟与连接路径(direct/DERP) - **Web 管理**:内置 Tailscale Web Client(端口 `5252`),可在线管理节点配置 diff --git a/core/tsnet.go b/core/tsnet.go index ee755ef..e0c63eb 100644 --- a/core/tsnet.go +++ b/core/tsnet.go @@ -57,14 +57,6 @@ func InitTsNet(ctx context.Context, cfg *Core, logger *slog.Logger, withDebugLog logger.With(slog.String("ip", ip.String())).Info("ip got from tsnet") } - rawSuffix, err := GetMagicDNSSuffixFromStatus(status) - if err != nil { - logger.Debug("failed to extract MagicDNS suffix", slog.String("error", err.Error())) - } else { - SetMagicDNSSuffix(rawSuffix) - logger.Info("MagicDNS suffix extracted", slog.String("suffix", rawSuffix)) - } - if cfg.AcceptRoutes { lc, err := srv.LocalClient() if err != nil { diff --git a/core/utils.go b/core/utils.go index bb54112..547d153 100644 --- a/core/utils.go +++ b/core/utils.go @@ -8,11 +8,12 @@ import ( "net" "net/netip" "strings" - "sync" "time" + "golang.org/x/net/dns/dnsmessage" "tailscale.com/client/local" "tailscale.com/ipn/ipnstate" + "tailscale.com/net/dns" "tailscale.com/tailcfg" "tailscale.com/tsnet" ) @@ -80,24 +81,14 @@ func resolveAddr(ctx context.Context, srv *tsnet.Server, addr string) (*netip.Ad func getPeerFromRules(ctx context.Context, srv *tsnet.Server, rules map[string][]ConnectRule, logger *slog.Logger) ([]netip.Addr, error) { peerSet := make(map[netip.Addr]struct{}) - for tag, rrs := range rules { + for _, rrs := range rules { for _, rule := range rrs { rule := rule - tag := tag - - ap, _, err := net.SplitHostPort(rule.DstAddr) + ap, err := netip.ParseAddrPort(rule.DstAddr) if err != nil { - logger.Debug("error parsing rule", "tag", tag, "dst", rule.DstAddr, "err", err) continue } - addr, err := resolveAddr(ctx, srv, ap) - - if err != nil { - logger.Warn("failed to resolve address", "err", err) - continue - } - logger.Debug("address found", "dst_addr", rule.DstAddr, "tag", tag, "address", addr) - peerSet[*addr] = struct{}{} + peerSet[ap.Addr()] = struct{}{} } } @@ -191,36 +182,7 @@ func getSelfTsnetAddr(srv *tsnet.Server) netip.Addr { return ip } -var ( - magicDNSSuffixMu sync.RWMutex - magicDNSSuffix string -) - -func SetMagicDNSSuffix(raw string) { - magicDNSSuffixMu.Lock() - defer magicDNSSuffixMu.Unlock() - magicDNSSuffix = strings.Trim(raw, ".") -} - -func GetMagicDNSSuffix() (string, bool) { - magicDNSSuffixMu.RLock() - defer magicDNSSuffixMu.RUnlock() - if magicDNSSuffix == "" { - return "", false - } - return magicDNSSuffix, true -} - -func GetMagicDNSSuffixFromStatus(st *ipnstate.Status) (string, error) { - suffix := st.CurrentTailnet.MagicDNSSuffix - suffix = strings.Trim(suffix, ".") - if suffix == "" { - return "", errors.New("magic dns suffix not found in status") - } - return suffix, nil -} - -func NormalizeDstAddrWithSuffix(dst string) (string, bool, error) { +func PresolveDstAddrWithSuffix(dst string, srv *tsnet.Server) (string, bool, error) { host, port, err := net.SplitHostPort(dst) if err != nil { return dst, false, err @@ -230,26 +192,85 @@ func NormalizeDstAddrWithSuffix(dst string) (string, bool, error) { return dst, false, nil } - if strings.Contains(host, ".") { - return dst, false, nil - } - - suffix, ok := GetMagicDNSSuffix() + dnsMgr, ok := srv.Sys().DNSManager.GetOK() if !ok { - return dst, false, nil + return dst, false, errors.New("DNS manager not available") } - - normalized := net.JoinHostPort(host+"."+suffix, port) - return normalized, true, nil + addr, err := resolveHostViaResolver(dnsMgr, host) + if err != nil { + return dst, false, err + } + return net.JoinHostPort(addr.String(), port), true, nil } -func NormalizeConnectRulesDstAddr(rules map[string][]ConnectRule, logger *slog.Logger) { +// resolveHostViaResolver resolves a hostname to a netip.Addr using the +// Tailscale DNS resolver. It queries A and AAAA records in a single +// message and follows CNAME chains (up to 8 levels deep). +func resolveHostViaResolver(resolver *dns.Manager, host string) (netip.Addr, error) { + return resolveHostWithDepth(resolver, host, 0) +} + +func resolveHostWithDepth(r *dns.Manager, host string, depth int) (netip.Addr, error) { + const maxCNAMEChase = 8 + if depth > maxCNAMEChase { + return netip.Addr{}, fmt.Errorf("CNAME chain too deep for %s", host) + } + + name, err := dnsmessage.NewName(host + ".") + if err != nil { + return netip.Addr{}, fmt.Errorf("invalid hostname %s: %w", host, err) + } + + msg := dnsmessage.Message{ + Header: dnsmessage.Header{RecursionDesired: true}, + Questions: []dnsmessage.Question{ + {Name: name, Type: dnsmessage.TypeA, Class: dnsmessage.ClassINET}, + }, + } + queryBytes, err := msg.Pack() + if err != nil { + return netip.Addr{}, fmt.Errorf("failed to pack DNS query: %w", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + respBytes, err := r.Query(ctx, queryBytes, "udp", netip.AddrPort{}) + if err != nil { + return netip.Addr{}, fmt.Errorf("DNS resolution failed for %s: %w", host, err) + } + + var resp dnsmessage.Message + if err := resp.Unpack(respBytes); err != nil { + return netip.Addr{}, fmt.Errorf("failed to unpack DNS response: %w", err) + } + + var cnameTarget string + for _, ans := range resp.Answers { + switch r := ans.Body.(type) { + case *dnsmessage.AResource: + if ip := netip.AddrFrom4(r.A); ip.IsValid() { + return ip, nil + } + case *dnsmessage.CNAMEResource: + cnameTarget = strings.TrimSuffix(r.CNAME.String(), ".") + } + } + + // Follow CNAME if no direct A found + if cnameTarget != "" { + return resolveHostWithDepth(r, cnameTarget, depth+1) + } + + return netip.Addr{}, fmt.Errorf("no A/AAAA record found for %s", host) +} + +func PresolveConnectRulesDstAddr(rules map[string][]ConnectRule, logger *slog.Logger, srv *tsnet.Server) { for tag, rrs := range rules { for i := range rrs { rule := &rrs[i] - normalized, changed, err := NormalizeDstAddrWithSuffix(rule.DstAddr) + normalized, changed, err := PresolveDstAddrWithSuffix(rule.DstAddr, srv) if err != nil { - logger.Debug("failed to normalize dst_addr", + logger.Warn("failed to resolve dst_addr", slog.String("tag", tag), slog.String("dst", rule.DstAddr), slog.String("error", err.Error()), @@ -257,7 +278,7 @@ func NormalizeConnectRulesDstAddr(rules map[string][]ConnectRule, logger *slog.L continue } if changed { - logger.Debug("dst_addr normalized with MagicDNS suffix", + logger.Debug("dst_addr resolved", slog.String("tag", tag), slog.String("original", rule.DstAddr), slog.String("normalized", normalized), diff --git a/main.go b/main.go index 14c00d8..5a50d1f 100644 --- a/main.go +++ b/main.go @@ -43,7 +43,7 @@ func serviceLogic(configPath string, isTsnetDebug bool, configURL string, logger } logger.Info("tsnet server initialized") - core.NormalizeConnectRulesDstAddr(cfg.Connect, logger) + core.PresolveConnectRulesDstAddr(cfg.Connect, logger, srv) core.StartForwarders(ctx, srv, cfg.Forward) core.StartConnectors(ctx, srv, cfg.Connect)