diff --git a/core/utils.go b/core/utils.go index 9f223e2..7841e5a 100644 --- a/core/utils.go +++ b/core/utils.go @@ -2,8 +2,17 @@ package core import ( "context" + "errors" + "fmt" "log/slog" + "net" + "net/netip" + "strings" "time" + + "tailscale.com/client/local" + "tailscale.com/tailcfg" + "tailscale.com/tsnet" ) func StartTimeWatchDog(ctx context.Context, logger *slog.Logger) <-chan struct{} { @@ -27,11 +36,127 @@ func StartTimeWatchDog(ctx context.Context, logger *slog.Logger) <-chan struct{} ch <- struct{}{} logger.Debug("signal sent, watchdog exiting...") return - } else { - lastUnix = nowUnix } + lastUnix = nowUnix } } }() return ch } + +func resolveAddr(ctx context.Context, srv *tsnet.Server, addr string) (*netip.Addr, error) { + lc, err := srv.LocalClient() + if err != nil { + return nil, err + } + stat, err := lc.Status(ctx) + if err != nil { + return nil, err + } + + if ip, err := netip.ParseAddr(addr); err == nil { + for _, peer := range stat.Peer { + for _, ipRange := range peer.AllowedIPs.All() { + if ipRange.Contains(ip) { + return &peer.TailscaleIPs[0], nil + } + } + } + } else { + // addr is domain, resolve it + for _, peer := range stat.Peer { + dnsName := strings.TrimSuffix(peer.DNSName, ".") + if dnsName == addr { + return &peer.TailscaleIPs[0], nil + } + } + } + + return nil, errors.New(fmt.Sprintf("addr '%s' not found in tsnet", addr)) +} + +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 _, rule := range rrs { + rule := rule + tag := tag + + ap, _, err := net.SplitHostPort(rule.DstAddr) + if err != nil { + logger.Debug("error parsing rule", "tag", tag, "dst", rule.DstAddr, "err", err) + // err log + 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{}{} + } + } + + var result []netip.Addr + for peer := range peerSet { + result = append(result, peer) + } + return result, nil +} + +func peerConnectivityLogic(ctx context.Context, lc *local.Client, relativePeers []netip.Addr, logger *slog.Logger) { + for _, peer := range relativePeers { + ping, err := lc.Ping(ctx, peer, tailcfg.PingDisco) + if err != nil { + logger.Debug("connectivity: failed to ping peer", "peer", peer, "err", err) + return + } + var connect string + if ping.DERPRegionCode == "" { + connect = "direct" + } else { + connect = ping.DERPRegionCode + } + logger.Info("connectivity: peer pinged", + "peer", peer, + "latency", fmt.Sprintf("%.2fms", ping.LatencySeconds*1000), + "connect", connect, + ) + } +} + +func StartPeerConnectivityDiagnostics(ctx context.Context, logger *slog.Logger, srv *tsnet.Server, rules map[string][]ConnectRule) { + relativePeers, err := getPeerFromRules(ctx, srv, rules, logger) + if err != nil { + return + } + logger.Debug("Peers loaded", "count", len(relativePeers)) + + if len(relativePeers) == 0 { + return + } + go func() { + lc, err := srv.LocalClient() + if err != nil { + logger.Error("failed to get local client", "err", err) + return + } + + ticker := time.NewTicker(120 * time.Second) + defer ticker.Stop() + + peerConnectivityLogic(ctx, lc, relativePeers, logger) // execute now + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + peerConnectivityLogic(ctx, lc, relativePeers, logger) + } + } + }() +} diff --git a/main.go b/main.go index 9a3bc30..13787fa 100644 --- a/main.go +++ b/main.go @@ -35,6 +35,8 @@ func serviceLogic(configPath string, logger *slog.Logger) bool { core.RunLanDiscoverService(ctx, cfg.Connect, logger.With("from", "lan_service")) + core.StartPeerConnectivityDiagnostics(ctx, logger, srv, cfg.Connect) + sigHandler := make(chan os.Signal, 1) signal.Notify(sigHandler, os.Interrupt, syscall.SIGTERM)