fix: use correct tailscale dns behaviour
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
- **双向转发**:`forward`(Tailscale → 本地)与 `connect`(本地 → Tailscale)两种模式
|
||||
- **TCP / UDP 全支持**:透明转发 TCP 流与 UDP 数据包
|
||||
- **Minecraft 专用模式**:支持局域网广播发现(MOTD),让本地设备发现 Tailnet 上的 Minecraft 服务器
|
||||
- **MagicDNS 主机名补全**:`dst_addr` 支持短主机名(如 `home:8080`),启动时自动补全为 `home.<suffix>:8080`
|
||||
- **MagicDNS 主机名补全**:`dst_addr` 支持按照 Tailscale 规则正确解析 Split DNS 和 Magic DNS
|
||||
- **连接类型识别**:区分 `direct` 直连与 `derp` 中继,便于排查延迟问题
|
||||
- **对端连通性诊断**:定期 ping 目标节点并报告延迟与连接路径(direct/DERP)
|
||||
- **Web 管理**:内置 Tailscale Web Client(端口 `5252`),可在线管理节点配置
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+78
-57
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user