From d12ad615cf27240be008fd5f5ffafac4adf595be Mon Sep 17 00:00:00 2001 From: nullcat Date: Sun, 24 May 2026 23:37:31 +0800 Subject: [PATCH 01/22] feat(utils): add timeout on ping request --- core/utils.go | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/core/utils.go b/core/utils.go index 7841e5a..f3add4f 100644 --- a/core/utils.go +++ b/core/utils.go @@ -11,6 +11,7 @@ import ( "time" "tailscale.com/client/local" + "tailscale.com/ipn/ipnstate" "tailscale.com/tailcfg" "tailscale.com/tsnet" ) @@ -109,11 +110,23 @@ func getPeerFromRules(ctx context.Context, srv *tsnet.Server, rules map[string][ 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) + ping, err := func() (*ipnstate.PingResult, error) { + cnclCtx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + + ping, err := lc.Ping(cnclCtx, peer, tailcfg.PingDisco) + return ping, err + }() + if err != nil { - logger.Debug("connectivity: failed to ping peer", "peer", peer, "err", err) + if errors.Is(err, context.DeadlineExceeded) { + logger.Warn("connectivity: peer ping timeout", "peer", peer) + } else { + logger.Warn("connectivity: failed to ping peer", "peer", peer, "err", err) + } return } + var connect string if ping.DERPRegionCode == "" { connect = "direct" From 7e8d005f5824a1b316177002ecd816a6bd0a8848 Mon Sep 17 00:00:00 2001 From: nullcat Date: Mon, 25 May 2026 00:04:49 +0800 Subject: [PATCH 02/22] feat(main): add -diagnose flag to control tsnet debug log --- core/tsnet.go | 14 ++++++++++---- main.go | 9 +++++---- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/core/tsnet.go b/core/tsnet.go index ea99b5d..ed2d18c 100644 --- a/core/tsnet.go +++ b/core/tsnet.go @@ -10,14 +10,20 @@ import ( "tailscale.com/tsnet" ) -func InitTsNet(ctx context.Context, cfg *Core, logger *slog.Logger) (*tsnet.Server, error) { +func InitTsNet(ctx context.Context, cfg *Core, logger *slog.Logger, withDebugLog bool) (*tsnet.Server, error) { + dbgLogger := func(fmt string, args ...interface{}) {} + if withDebugLog { + logger.Warn("Tsnet debug log activated") + dbgLogger = func(fmt string, args ...interface{}) { + logger.With(slog.String("from", "tsnet")).Debug(fmt2.Sprintf(fmt, args...)) + } + } + srv := &tsnet.Server{ Hostname: "tslink-" + cfg.Hostname, AuthKey: cfg.AuthKey, Ephemeral: cfg.Ephemeral, - Logf: func(fmt string, args ...interface{}) { - logger.With(slog.String("from", "tsnet")).Debug(fmt2.Sprintf(fmt, args...)) - }, + Logf: dbgLogger, UserLogf: func(fmt string, args ...interface{}) { logger.With(slog.String("from", "tsnet")).Info(fmt2.Sprintf(fmt, args...)) }, diff --git a/main.go b/main.go index 13787fa..9e38d7a 100644 --- a/main.go +++ b/main.go @@ -11,7 +11,7 @@ import ( "tslink/core" ) -func serviceLogic(configPath string, logger *slog.Logger) bool { +func serviceLogic(configPath string, isTsnetDebug bool, logger *slog.Logger) bool { cfg, err := core.LoadConfig(configPath) if err != nil { logger.With( @@ -22,7 +22,7 @@ func serviceLogic(configPath string, logger *slog.Logger) bool { ctx, cancelAll := context.WithCancel(context.Background()) logger.Info("initializing tsnet server") - srv, err := core.InitTsNet(ctx, &cfg.Core, logger) + srv, err := core.InitTsNet(ctx, &cfg.Core, logger, isTsnetDebug) if err != nil { logger.With( slog.String("error", err.Error())).Error("Error initializing tsnet") @@ -45,7 +45,7 @@ func serviceLogic(configPath string, logger *slog.Logger) bool { go func() { <-ctx.Done() logger.Debug("stopping tsnet service") - srv.Close() + _ = srv.Close() // just ignore it }() for { @@ -66,6 +66,7 @@ func serviceLogic(configPath string, logger *slog.Logger) bool { func main() { useJsonFormatLogger := flag.Bool("json-format", false, "use json format logger") + showTsnetDebugLog := flag.Bool("diagnose", false, "show tsnet debug log on level=debug") logLevel := flag.String("level", "info", "log level (DEBUG|INFO|WARN|ERROR)") configPath := flag.String("c", "config.toml", "path to config file") flag.Parse() @@ -75,7 +76,7 @@ func main() { logger.Info("Starting tslink server", "level", *logLevel, "configPath", *configPath) for { - isStopped := serviceLogic(*configPath, logger) + isStopped := serviceLogic(*configPath, *showTsnetDebugLog, logger) if !isStopped { logger.Warn("tslink server restart") } else { From c23ca5025c19844618cebfee2b5de3e3f333eba9 Mon Sep 17 00:00:00 2001 From: nullcat Date: Mon, 25 May 2026 00:07:01 +0800 Subject: [PATCH 03/22] fix(forward): udp relay not using tsnet server context --- core/forwarder.go | 147 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 133 insertions(+), 14 deletions(-) diff --git a/core/forwarder.go b/core/forwarder.go index e119ed1..fde3085 100644 --- a/core/forwarder.go +++ b/core/forwarder.go @@ -6,6 +6,7 @@ import ( "io" "log/slog" "net" + "net/netip" "sync" "time" @@ -182,27 +183,114 @@ func runUDPForwarder(ctx context.Context, srv *tsnet.Server, rule ForwardRule, l if !ip.IsValid() { ip = ip6 } - pc, err := srv.ListenPacket("udp", fmt.Sprintf("%s:%d", ip.String(), rule.TailscalePort)) + ln, err := srv.Listen("udp", fmt.Sprintf("%s:%d", ip.String(), rule.TailscalePort)) if err != nil { logger.Error("failed to listen", "error", err) return } logger.Info("listening", slog.String("on", fmt.Sprintf("tailscale:%s:%d", ip.String(), rule.TailscalePort))) - relay := &udpRelay{ - listenConn: pc, - dialAddr: rule.LocalAddr, - logger: logger, - direction: "local", - sessions: make(map[string]*udpSession), + go func() { + <-ctx.Done() + ln.Close() + }() + + for { + conn, err := ln.Accept() + if err != nil { + if ctx.Err() != nil { + return + } + logger.Error("accept error", "error", err) + continue + } + go handleUDPForward(ctx, srv, conn, rule, logger) } - relay.run(ctx) +} + +func handleUDPForward(ctx context.Context, srv *tsnet.Server, conn net.Conn, rule ForwardRule, logger *slog.Logger) { + remoteAddrStr := conn.RemoteAddr().String() + clog := logger.With(slog.String("remote", remoteAddrStr)) + + lc, err := srv.LocalClient() + if err == nil { + who, err := lc.WhoIs(ctx, remoteAddrStr) + if err == nil { + clog = clog.With(slog.String("user", who.UserProfile.LoginName)) + } + } + + connType := getConnType(ctx, srv, remoteAddrStr) + clog.Info("accepted connection", + slog.String("conn_type", connType), + slog.String("local_addr", rule.LocalAddr), + ) + + defer conn.Close() + + localConn, err := net.Dial("udp", rule.LocalAddr) + if err != nil { + clog.Error("failed to dial local", "error", err) + return + } + defer localConn.Close() + + remoteIP, _, _ := net.SplitHostPort(remoteAddrStr) + + var toTs, toLocal int64 + var wg sync.WaitGroup + wg.Add(2) + + go func() { + defer wg.Done() + buf := make([]byte, 65535) + for { + n, err := conn.Read(buf) + if err != nil { + return + } + toLocal += int64(n) + clog.Debug("inbound udp packet", + slog.String("from_ip", remoteIP), + slog.String("to_ip", rule.LocalAddr), + slog.Int("pkg_size", n), + ) + if _, err := localConn.Write(buf[:n]); err != nil { + return + } + } + }() + + go func() { + defer wg.Done() + buf := make([]byte, 65535) + for { + n, err := localConn.Read(buf) + if err != nil { + return + } + toTs += int64(n) + localIP, _, _ := net.SplitHostPort(localConn.RemoteAddr().String()) + clog.Debug("outbound udp packet", + slog.String("from_ip", localIP), + slog.String("to_ip", remoteIP), + slog.Int("pkg_size", n), + ) + if _, err := conn.Write(buf[:n]); err != nil { + return + } + } + }() + + wg.Wait() + clog.Info("connection closed", slog.Int64("ts_rx_bytes", toLocal), slog.Int64("ts_tx_bytes", toTs)) } type udpSession struct { - conn net.Conn - remote net.Addr - lastUse time.Time + conn net.Conn + remote net.Addr + inTailscale bool + lastUse time.Time } type udpRelay struct { @@ -210,6 +298,8 @@ type udpRelay struct { dialAddr string logger *slog.Logger direction string + srv *tsnet.Server + ctx context.Context mu sync.Mutex sessions map[string]*udpSession @@ -255,23 +345,43 @@ func (r *udpRelay) run(ctx context.Context) { r.mu.Lock() sess, exists := r.sessions[key] if !exists { - dialed, err := net.Dial("udp", r.dialAddr) + inTsnet := false + if host, _, err := net.SplitHostPort(r.dialAddr); err == nil { + if dialIP, err := netip.ParseAddr(host); err == nil { + tsnetCIDR := netip.MustParsePrefix("100.64.0.0/10") + inTsnet = tsnetCIDR.Contains(dialIP) + } + } + + var dialed net.Conn + if inTsnet { + dialed, err = r.srv.Dial(r.ctx, "udp", r.dialAddr) + } else { + dialed, err = net.Dial("udp", r.dialAddr) + } if err != nil { r.mu.Unlock() r.logger.Error("failed to dial", "error", err) continue } - sess = &udpSession{conn: dialed, remote: from, lastUse: time.Now()} + sess = &udpSession{conn: dialed, remote: from, lastUse: time.Now(), inTailscale: inTsnet} r.sessions[key] = sess r.mu.Unlock() - r.logger.Debug("new udp session", slog.String("remote", key), slog.String("direction", r.direction)) + r.logger.Info("new udp session", slog.String("remote", key), slog.String("direction", r.direction)) go r.readSession(key, sess) } else { sess.lastUse = time.Now() r.mu.Unlock() } + fromIP, _, _ := net.SplitHostPort(from.String()) + toIP, _, _ := net.SplitHostPort(sess.conn.RemoteAddr().String()) + r.logger.Debug("outbound udp packet", + slog.String("from_ip", fromIP), + slog.String("to_ip", toIP), + slog.Int("pkg_size", n), + ) if _, err := sess.conn.Write(buf[:n]); err != nil { r.logger.Error("failed to write", "error", err) r.removeSession(key) @@ -287,6 +397,13 @@ func (r *udpRelay) readSession(key string, sess *udpSession) { r.removeSession(key) return } + fromIP, _, _ := net.SplitHostPort(sess.conn.RemoteAddr().String()) + toIP, _, _ := net.SplitHostPort(sess.remote.String()) + r.logger.Info("udp packet", + slog.String("from_ip", fromIP), + slog.String("to_ip", toIP), + slog.Int("pkg_size", n), + ) if _, err := r.listenConn.WriteTo(buf[:n], sess.remote); err != nil { r.logger.Error("failed to write back", "error", err) r.removeSession(key) @@ -406,6 +523,8 @@ func runUDPConnector(ctx context.Context, srv *tsnet.Server, rule ConnectRule, l dialAddr: rule.DstAddr, logger: logger, direction: "tailscale", + srv: srv, + ctx: ctx, sessions: make(map[string]*udpSession), } relay.run(ctx) From 8435d283f90253185cb5ee25ec1eb61d5db66ff3 Mon Sep 17 00:00:00 2001 From: nullcat Date: Mon, 25 May 2026 00:15:29 +0800 Subject: [PATCH 04/22] chore(ci): add GitHub Actions workflow for cross-platform Go build --- .github/workflows/build.yml | 45 +++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..a413e69 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,45 @@ +name: Build + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + + build: + runs-on: ubuntu-latest + strategy: + matrix: + goos: [linux, windows, darwin] + goarch: [amd64, arm64] + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.26.3' + cache: true + + - name: Install dependencies + run: go mod download + + - name: Build + env: + GOOS: ${{ matrix.goos }} + GOARCH: ${{ matrix.goarch }} + run: | + DATE=$(date +'%y%m%d') + BINARY_NAME="tslink-dev${DATE}-${{ matrix.goos }}_${{ matrix.goarch }}" + if [ "${{ matrix.goos }}" = "windows" ]; then + BINARY_NAME="${BINARY_NAME}.exe" + fi + go build -v -o "release/${BINARY_NAME}" . + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: tslink-${{ matrix.goos }}-${{ matrix.goarch }} + path: release/ From 284111360c12287e7b59215980871b50f72827fb Mon Sep 17 00:00:00 2001 From: nullcat Date: Mon, 25 May 2026 00:21:49 +0800 Subject: [PATCH 05/22] chore: add MIT license file --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e1d12a9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 nullcat + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From 8913f8abda0e61a50f49c47dd2183faa28265f8b Mon Sep 17 00:00:00 2001 From: nullcat Date: Mon, 25 May 2026 00:40:50 +0800 Subject: [PATCH 06/22] fix(ci): not working --- .github/workflows/build.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a413e69..dd47325 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,12 +2,11 @@ name: Build on: push: - branches: [ "main" ] + branches: + - main pull_request: - branches: [ "main" ] jobs: - build: runs-on: ubuntu-latest strategy: From 808bcd121a4b7e4dd573866de8d534633ab2f307 Mon Sep 17 00:00:00 2001 From: nullcat Date: Mon, 25 May 2026 00:47:06 +0800 Subject: [PATCH 07/22] fix(ci): wrong push trigger --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dd47325..bb6971d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,7 +3,7 @@ name: Build on: push: branches: - - main + - master pull_request: jobs: From 6d0243708733c965bb25623de4457947cb52b63e Mon Sep 17 00:00:00 2001 From: nullcat Date: Mon, 25 May 2026 00:50:08 +0800 Subject: [PATCH 08/22] fix(ci): add missing go.sum --- go.sum | 237 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 237 insertions(+) create mode 100644 go.sum diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f6518a2 --- /dev/null +++ b/go.sum @@ -0,0 +1,237 @@ +9fans.net/go v0.0.8-0.20250307142834-96bdba94b63f h1:1C7nZuxUMNz7eiQALRfiqNOm04+m3edWlRff/BYHf0Q= +9fans.net/go v0.0.8-0.20250307142834-96bdba94b63f/go.mod h1:hHyrZRryGqVdqrknjq5OWDLGCTJ2NeEvtrpR96mjraM= +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= +filippo.io/mkcert v1.4.4 h1:8eVbbwfVlaqUM7OwuftKc2nuYOoTDQWqsoXmzoXZdbc= +filippo.io/mkcert v1.4.4/go.mod h1:VyvOchVuAye3BoUsPUOOofKygVwLV2KQMVFJNRq+1dA= +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A= +github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw= +github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= +github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4= +github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= +github.com/aws/aws-sdk-go-v2/config v1.29.5 h1:4lS2IB+wwkj5J43Tq/AwvnscBerBJtQQ6YS7puzCI1k= +github.com/aws/aws-sdk-go-v2/config v1.29.5/go.mod h1:SNzldMlDVbN6nWxM7XsUiNXPSa1LWlqiXtvh/1PrJGg= +github.com/aws/aws-sdk-go-v2/credentials v1.17.58 h1:/d7FUpAPU8Lf2KUdjniQvfNdlMID0Sd9pS23FJ3SS9Y= +github.com/aws/aws-sdk-go-v2/credentials v1.17.58/go.mod h1:aVYW33Ow10CyMQGFgC0ptMRIqJWvJ4nxZb0sUiuQT/A= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27 h1:7lOW8NUwE9UZekS1DYoiPdVAqZ6A+LheHWb+mHbNOq8= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27/go.mod h1:w1BASFIPOPUae7AgaH4SbjNbfdkxuggLyGfNFTn8ITY= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 h1:Pg9URiobXy85kgFev3og2CuOZ8JZUBENF+dcgWBaYNk= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM= +github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7 h1:a8HvP/+ew3tKwSXqL3BCSjiuicr+XTU2eFYeogV9GJE= +github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7/go.mod h1:Q7XIWsMo0JcMpI/6TGD6XXcXcV1DbTj6e9BKNntIMIM= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.14 h1:c5WJ3iHz7rLIgArznb3JCSQT3uUMiz9DLZhIX+1G8ok= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.14/go.mod h1:+JJQTxB6N4niArC14YNtxcQtwEqzS3o9Z32n7q33Rfs= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13 h1:f1L/JtUkVODD+k1+IiSJUUv8A++2qVr+Xvb3xWXETMU= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13/go.mod h1:tvqlFoja8/s0o+UruA1Nrezo/df0PzdunMDDurUfg6U= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk= +github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= +github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= +github.com/axiomhq/hyperloglog v0.0.0-20240319100328-84253e514e02 h1:bXAPYSbdYbS5VTy92NIUbeDI1qyggi+JYh5op9IFlcQ= +github.com/axiomhq/hyperloglog v0.0.0-20240319100328-84253e514e02/go.mod h1:k08r+Yj1PRAmuayFiRK6MYuR5Ve4IuZtTfxErMIh0+c= +github.com/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok= +github.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE= +github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= +github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= +github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0= +github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= +github.com/creachadair/mds v0.25.9 h1:080Hr8laN2h+l3NeVCGMBpXtIPnl9mz8e4HLraGPqtA= +github.com/creachadair/mds v0.25.9/go.mod h1:4hatI3hRM+qhzuAmqPRFvaBM8mONkS7nsLxkcuTYUIs= +github.com/creachadair/msync v0.7.1 h1:SeZmuEBXQPe5GqV/C94ER7QIZPwtvFbeQiykzt/7uho= +github.com/creachadair/msync v0.7.1/go.mod h1:8CcFlLsSujfHE5wWm19uUBLHIPDAUr6LXDwneVMO008= +github.com/creachadair/taskgroup v0.13.2 h1:3KyqakBuFsm3KkXi/9XIb0QcA8tEzLHLgaoidf0MdVc= +github.com/creachadair/taskgroup v0.13.2/go.mod h1:i3V1Zx7H8RjwljUEeUWYT30Lmb9poewSb2XI1yTwD0g= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= +github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbwwpmHn1J5i43Y0uZP97GqasGCzSRJk= +github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa/go.mod h1:Nx87SkVqTKd8UtT+xu7sM/l+LgXs6c0aHrlKusR+2EQ= +github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc h1:8WFBn63wegobsYAX0YjD+8suexZDga5CctH4CCTx2+8= +github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw= +github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q= +github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A= +github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= +github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/gaissmai/bart v0.26.1 h1:+w4rnLGNlA2GDVn382Tfe3jOsK5vOr5n4KmigJ9lbTo= +github.com/gaissmai/bart v0.26.1/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c= +github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I= +github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo= +github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced h1:Q311OHjMh/u5E2TITc++WlTP5We0xNseRMkHDyvhW7I= +github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go4org/plan9netshell v0.0.0-20250324183649-788daa080737 h1:cf60tHxREO3g1nroKr2osU3JWZsJzkfi7rEg+oAB0Lo= +github.com/go4org/plan9netshell v0.0.0-20250324183649-788daa080737/go.mod h1:MIS0jDzbU/vuM9MC4YnBITCv+RYuTRq8dJzmCrFsK9g= +github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg= +github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466/go.mod h1:ZiQxhyQ+bbbfxUKVvjfO498oPYvtYhZzycal3G/NHmU= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-tpm v0.9.4 h1:awZRf9FwOeTunQmHoDYSHJps3ie6f1UlhS1fOdPEt1I= +github.com/google/go-tpm v0.9.4/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= +github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI= +github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU= +github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/illarion/gonotify/v3 v3.0.2 h1:O7S6vcopHexutmpObkeWsnzMJt/r1hONIEogeVNmJMk= +github.com/illarion/gonotify/v3 v3.0.2/go.mod h1:HWGPdPe817GfvY3w7cx6zkbzNZfi3QjcBm/wgVvEL1U= +github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 h1:9K06NfxkBh25x56yVhWWlKFE8YpicaSfHwoV8SFbueA= +github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI= +github.com/jellydator/ttlcache/v3 v3.1.0 h1:0gPFG0IHHP6xyUyXq+JaD8fwkDCqgqwohXNJBcYE71g= +github.com/jellydator/ttlcache/v3 v3.1.0/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVKySzCVW6+0gA2n4= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jsimonetti/rtnetlink v1.4.0 h1:Z1BF0fRgcETPEa0Kt0MRk3yV5+kF1FWTni6KUFKrq2I= +github.com/jsimonetti/rtnetlink v1.4.0/go.mod h1:5W1jDvWdnthFJ7fxYX1GMK07BUpI4oskfOqvPteYS6E= +github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= +github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ= +github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk= +github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lmittmann/tint v1.1.3 h1:Hv4EaHWXQr+GTFnOU4VKf8UvAtZgn0VuKT+G0wFlO3I= +github.com/lmittmann/tint v1.1.3/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw= +github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o= +github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 h1:A1Cq6Ysb0GM0tpKMbdCXCIfBclan4oHk1Jb+Hrejirg= +github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42/go.mod h1:BB4YCPDOzfy7FniQ/lxuYQ3dgmM2cZumHbK8RpTjN2o= +github.com/mdlayher/sdnotify v1.0.0 h1:Ma9XeLVN/l0qpyx1tNeMSeTjCPH6NtuD6/N9XdTlQ3c= +github.com/mdlayher/sdnotify v1.0.0/go.mod h1:HQUmpM4XgYkhDLtd+Uad8ZFK1T9D5+pNxnXQjCeJlGE= +github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI= +github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI= +github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4= +github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY= +github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= +github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0= +github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= +github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0= +github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= +github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo= +github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= +github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0= +github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs= +github.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d h1:JcGKBZAL7ePLwOhUdN8qGQZlP5GueEiIZwY7R62pejE= +github.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4= +github.com/tailscale/gliderssh v0.3.4-0.20260330083525-c1389c70ff89 h1:glgVc1ZYMjwN1Q/ITWeuSQyl029uayagaR2sjsifehc= +github.com/tailscale/gliderssh v0.3.4-0.20260330083525-c1389c70ff89/go.mod h1:wn16Km1EZOX4UEAyaZa3dBwfFGOJ7neck40NcwosJUw= +github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4= +github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg= +github.com/tailscale/golang-x-crypto v0.0.0-20250404221719-a5573b049869 h1:SRL6irQkKGQKKLzvQP/ke/2ZuB7Py5+XuqtOgSj+iMM= +github.com/tailscale/golang-x-crypto v0.0.0-20250404221719-a5573b049869/go.mod h1:ikbF+YT089eInTp9f2vmvy4+ZVnW5hzX1q2WknxSprQ= +github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd h1:Rf9uhF1+VJ7ZHqxrG8pJ6YacmHvVCmByDmGbAWCc/gA= +github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd/go.mod h1:EbW0wDK/qEUYI0A5bqq0C2kF8JTQwWONmGDBbzsxxHo= +github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 h1:uFsXVBE9Qr4ZoF094vE6iYTLDl0qCiKzYXlL6UeWObU= +github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0= +github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc h1:24heQPtnFR+yfntqhI3oAu9i27nEojcQ4NuBQOo5ZFA= +github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc/go.mod h1:f93CXfllFsO9ZQVq+Zocb1Gp4G5Fz0b0rXHLOzt/Djc= +github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 h1:UBPHPtv8+nEAy2PD8RyAhOYvau1ek0HDJqLS/Pysi14= +github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ= +github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6 h1:l10Gi6w9jxvinoiq15g8OToDdASBni4CyJOdHY1Hr8M= +github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6/go.mod h1:ZXRML051h7o4OcI0d3AaILDIad/Xw0IkXaHM17dic1Y= +github.com/tailscale/wireguard-go v0.0.0-20260427181203-e3ac4a0afb4e h1:GexFR7ak1iz26fxg8HWCpOEqAOL8UEZJ7J3JxeCalDs= +github.com/tailscale/wireguard-go v0.0.0-20260427181203-e3ac4a0afb4e/go.mod h1:6SerzcvHWQchKO2BfNdmquA77CHSECZuFl+D9fp4RnI= +github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e h1:zOGKqN5D5hHhiYUp091JqK7DPCqSARyUfduhGUY8Bek= +github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg= +github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA= +github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk= +github.com/u-root/u-root v0.14.0 h1:Ka4T10EEML7dQ5XDvO9c3MBN8z4nuSnGjcd1jmU2ivg= +github.com/u-root/u-root v0.14.0/go.mod h1:hAyZorapJe4qzbLWlAkmSVCJGbfoU9Pu4jpJ1WMluqE= +github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM= +github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA= +github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= +github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek= +go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g= +go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= +go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= +golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f h1:phY1HzDcf18Aq9A8KkmRtY9WvOFIxN8wgfvy6Zm1DV8= +golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= +golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w= +golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g= +golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= +golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= +golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= +golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= +golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= +golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE= +golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8 h1:Zy8IV/+FMLxy6j6p87vk/vQGKcdnbprwjTxc8UiUtsA= +gvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8/go.mod h1:QkHjoMIBaYtpVufgwv3keYAbln78mBoCuShZrPrer1Q= +honnef.co/go/tools v0.7.0 h1:w6WUp1VbkqPEgLz4rkBzH/CSU6HkoqNLp6GstyTx3lU= +honnef.co/go/tools v0.7.0/go.mod h1:pm29oPxeP3P82ISxZDgIYeOaf9ta6Pi0EWvCFoLG2vc= +howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= +howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= +software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= +software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= +tailscale.com v1.98.2 h1:HP5gt0qyLKtJoDV7PMUvPpiXjMFk4nzXMbm7JdjttMY= +tailscale.com v1.98.2/go.mod h1:U23ZwbZlKJMNU7CScy+lCVVlece/S5n09q0nyudncBI= From 7bf362cc6617c02ea3f255922ffac2470f3aeda9 Mon Sep 17 00:00:00 2001 From: nullcat Date: Mon, 25 May 2026 00:59:41 +0800 Subject: [PATCH 09/22] chore(ci): compress artifact with upx and update binary name --- .github/workflows/build.yml | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bb6971d..99928b9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,15 +30,26 @@ jobs: GOOS: ${{ matrix.goos }} GOARCH: ${{ matrix.goarch }} run: | - DATE=$(date +'%y%m%d') - BINARY_NAME="tslink-dev${DATE}-${{ matrix.goos }}_${{ matrix.goarch }}" + BINARY_NAME="tslink" if [ "${{ matrix.goos }}" = "windows" ]; then BINARY_NAME="${BINARY_NAME}.exe" fi go build -v -o "release/${BINARY_NAME}" . + - name: Compress with UPX + uses: svenstaro/upx-action@v2 + with: + files: | + release/tslink* + args: "-9" + strip: true + + - name: Get current date + id: date + run: echo "date=$(date +'%y%m%d')" >> $GITHUB_OUTPUT + - name: Upload Artifact uses: actions/upload-artifact@v4 with: - name: tslink-${{ matrix.goos }}-${{ matrix.goarch }} + name: tslink-dev${{ steps.date.outputs.date }}-${{ matrix.goos }}_${{ matrix.goarch }} path: release/ From fb6965cd9425855048006f591a9a19a9df3f7e99 Mon Sep 17 00:00:00 2001 From: nullcat Date: Mon, 25 May 2026 01:07:10 +0800 Subject: [PATCH 10/22] fix(ci): upx compress runs on unsupported platforms --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 99928b9..7aaba60 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -37,12 +37,12 @@ jobs: go build -v -o "release/${BINARY_NAME}" . - name: Compress with UPX - uses: svenstaro/upx-action@v2 + if: matrix.goos != 'darwin' + uses: crazy-max/ghaction-upx@v4.0.0 with: files: | release/tslink* args: "-9" - strip: true - name: Get current date id: date From ad335bba719afdb93d663bc61e5b1dd914dd8c63 Mon Sep 17 00:00:00 2001 From: nullcat Date: Mon, 25 May 2026 01:12:17 +0800 Subject: [PATCH 11/22] fix(ci): upx compress runs on unsupported platforms --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7aaba60..465d3c8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -37,7 +37,7 @@ jobs: go build -v -o "release/${BINARY_NAME}" . - name: Compress with UPX - if: matrix.goos != 'darwin' + if: matrix.goos != 'darwin' && !(matrix.goos == 'windows' && matrix.goarch == 'arm64') uses: crazy-max/ghaction-upx@v4.0.0 with: files: | From 0ef729db725dda553a6af1d1b8e5977ed4b51e90 Mon Sep 17 00:00:00 2001 From: nullcat Date: Mon, 25 May 2026 01:18:21 +0800 Subject: [PATCH 12/22] chore(ci): add build flag for smaller size --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 465d3c8..019356b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,7 +34,7 @@ jobs: if [ "${{ matrix.goos }}" = "windows" ]; then BINARY_NAME="${BINARY_NAME}.exe" fi - go build -v -o "release/${BINARY_NAME}" . + go build -v -o -ldflags=”-s -w" "release/${BINARY_NAME}" . - name: Compress with UPX if: matrix.goos != 'darwin' && !(matrix.goos == 'windows' && matrix.goarch == 'arm64') From e72f03dfe29e37d2199ddd9dc762b5443b4391b8 Mon Sep 17 00:00:00 2001 From: nullcat Date: Mon, 25 May 2026 01:19:33 +0800 Subject: [PATCH 13/22] chore(ci): add build flag for smaller size --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 019356b..d45d66d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,7 +34,7 @@ jobs: if [ "${{ matrix.goos }}" = "windows" ]; then BINARY_NAME="${BINARY_NAME}.exe" fi - go build -v -o -ldflags=”-s -w" "release/${BINARY_NAME}" . + go build -v -o -ldflags="-s -w" "release/${BINARY_NAME}" . - name: Compress with UPX if: matrix.goos != 'darwin' && !(matrix.goos == 'windows' && matrix.goarch == 'arm64') From b4ba8e1de959224ebc5f15c25949e753ef36743b Mon Sep 17 00:00:00 2001 From: nullcat Date: Mon, 25 May 2026 01:24:10 +0800 Subject: [PATCH 14/22] chore(ci): add build flag for smaller size --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d45d66d..15ce43f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,7 +34,7 @@ jobs: if [ "${{ matrix.goos }}" = "windows" ]; then BINARY_NAME="${BINARY_NAME}.exe" fi - go build -v -o -ldflags="-s -w" "release/${BINARY_NAME}" . + go build -v -trimpath -buildvcs=false -o "release/${BINARY_NAME}" -ldflags="-s -w" . - name: Compress with UPX if: matrix.goos != 'darwin' && !(matrix.goos == 'windows' && matrix.goarch == 'arm64') From 702dd6bc24ce2b214745c77c97749f348b853f3c Mon Sep 17 00:00:00 2001 From: nullcat Date: Mon, 25 May 2026 01:43:20 +0800 Subject: [PATCH 15/22] chore(ci): update dependency --- .github/workflows/build.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 15ce43f..8a5e58d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,15 +9,17 @@ on: jobs: build: runs-on: ubuntu-latest + env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true strategy: matrix: goos: [linux, windows, darwin] goarch: [amd64, arm64] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version: '1.26.3' cache: true @@ -38,7 +40,7 @@ jobs: - name: Compress with UPX if: matrix.goos != 'darwin' && !(matrix.goos == 'windows' && matrix.goarch == 'arm64') - uses: crazy-max/ghaction-upx@v4.0.0 + uses: crazy-max/ghaction-upx@v4 with: files: | release/tslink* @@ -49,7 +51,7 @@ jobs: run: echo "date=$(date +'%y%m%d')" >> $GITHUB_OUTPUT - name: Upload Artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: tslink-dev${{ steps.date.outputs.date }}-${{ matrix.goos }}_${{ matrix.goarch }} path: release/ From f97540b8d0864be1e2d4cdcfdd52a2c227b11fc9 Mon Sep 17 00:00:00 2001 From: nullcat Date: Mon, 25 May 2026 15:12:31 +0800 Subject: [PATCH 16/22] chore(utils): add remote name into ping logger --- core/utils.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/core/utils.go b/core/utils.go index f3add4f..e7421af 100644 --- a/core/utils.go +++ b/core/utils.go @@ -87,7 +87,6 @@ func getPeerFromRules(ctx context.Context, srv *tsnet.Server, rules map[string][ 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) @@ -118,11 +117,19 @@ func peerConnectivityLogic(ctx context.Context, lc *local.Client, relativePeers return ping, err }() + loLog := logger.With("peer", peer) + peerInfo, err := lc.WhoIs(ctx, peer.String()) + if err != nil { + loLog.Warn("failed to get peer info", "err", err) + } else { + loLog = loLog.With("name", peerInfo.Node.ComputedName) + } + if err != nil { if errors.Is(err, context.DeadlineExceeded) { - logger.Warn("connectivity: peer ping timeout", "peer", peer) + loLog.Warn("connectivity: peer ping timeout") } else { - logger.Warn("connectivity: failed to ping peer", "peer", peer, "err", err) + loLog.Warn("connectivity: failed to ping peer", "err", err) } return } @@ -133,8 +140,7 @@ func peerConnectivityLogic(ctx context.Context, lc *local.Client, relativePeers } else { connect = ping.DERPRegionCode } - logger.Info("connectivity: peer pinged", - "peer", peer, + loLog.Info("connectivity: peer pinged", "latency", fmt.Sprintf("%.2fms", ping.LatencySeconds*1000), "connect", connect, ) From 4dde0942ce8c916efcc0f7d358981e423cae18de Mon Sep 17 00:00:00 2001 From: nullcat Date: Fri, 29 May 2026 14:31:32 +0800 Subject: [PATCH 17/22] fix(utils): panic on ping logic --- core/utils.go | 1 + 1 file changed, 1 insertion(+) diff --git a/core/utils.go b/core/utils.go index e7421af..682e6a7 100644 --- a/core/utils.go +++ b/core/utils.go @@ -121,6 +121,7 @@ func peerConnectivityLogic(ctx context.Context, lc *local.Client, relativePeers peerInfo, err := lc.WhoIs(ctx, peer.String()) if err != nil { loLog.Warn("failed to get peer info", "err", err) + continue } else { loLog = loLog.With("name", peerInfo.Node.ComputedName) } From c853084a7b99bf11d855d1d6558c1597d111e589 Mon Sep 17 00:00:00 2001 From: nullcat Date: Fri, 29 May 2026 14:56:17 +0800 Subject: [PATCH 18/22] fix(utils): move ping logic after peer info lookup and deduplicate address retrieval logic --- core/forwarder.go | 12 ++---------- core/utils.go | 28 ++++++++++++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/core/forwarder.go b/core/forwarder.go index fde3085..3dfc4ad 100644 --- a/core/forwarder.go +++ b/core/forwarder.go @@ -88,11 +88,7 @@ func runForwarder(ctx context.Context, srv *tsnet.Server, rule ForwardRule, tag } func runTCPForwarder(ctx context.Context, srv *tsnet.Server, rule ForwardRule, logger *slog.Logger) { - ip4, ip6 := srv.TailscaleIPs() - ip := ip4 - if !ip.IsValid() { - ip = ip6 - } + ip := getSelfTsnetAddr(srv) ln, err := srv.Listen("tcp", fmt.Sprintf("%s:%d", ip.String(), rule.TailscalePort)) if err != nil { logger.Error("failed to listen", "error", err) @@ -178,11 +174,7 @@ func getConnType(ctx context.Context, srv *tsnet.Server, remoteAddrStr string) s } func runUDPForwarder(ctx context.Context, srv *tsnet.Server, rule ForwardRule, logger *slog.Logger) { - ip4, ip6 := srv.TailscaleIPs() - ip := ip4 - if !ip.IsValid() { - ip = ip6 - } + ip := getSelfTsnetAddr(srv) ln, err := srv.Listen("udp", fmt.Sprintf("%s:%d", ip.String(), rule.TailscalePort)) if err != nil { logger.Error("failed to listen", "error", err) diff --git a/core/utils.go b/core/utils.go index 682e6a7..e6eabf7 100644 --- a/core/utils.go +++ b/core/utils.go @@ -109,6 +109,14 @@ func getPeerFromRules(ctx context.Context, srv *tsnet.Server, rules map[string][ func peerConnectivityLogic(ctx context.Context, lc *local.Client, relativePeers []netip.Addr, logger *slog.Logger) { for _, peer := range relativePeers { + loLog := logger.With("peer", peer) + peerInfo, err := lc.WhoIs(ctx, peer.String()) + if err != nil { + loLog.Warn("failed to get peer info", "err", err) + } else { + loLog = loLog.With("name", peerInfo.Node.ComputedName) + } + ping, err := func() (*ipnstate.PingResult, error) { cnclCtx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() @@ -117,22 +125,13 @@ func peerConnectivityLogic(ctx context.Context, lc *local.Client, relativePeers return ping, err }() - loLog := logger.With("peer", peer) - peerInfo, err := lc.WhoIs(ctx, peer.String()) - if err != nil { - loLog.Warn("failed to get peer info", "err", err) - continue - } else { - loLog = loLog.With("name", peerInfo.Node.ComputedName) - } - if err != nil { if errors.Is(err, context.DeadlineExceeded) { loLog.Warn("connectivity: peer ping timeout") } else { loLog.Warn("connectivity: failed to ping peer", "err", err) } - return + continue } var connect string @@ -180,3 +179,12 @@ func StartPeerConnectivityDiagnostics(ctx context.Context, logger *slog.Logger, } }() } + +func getSelfTsnetAddr(srv *tsnet.Server) netip.Addr { + ip4, ip6 := srv.TailscaleIPs() + ip := ip4 + if !ip.IsValid() { + ip = ip6 + } + return ip +} From bcbe8fb5e34906ed40b40b4226914ac171c6a9b9 Mon Sep 17 00:00:00 2001 From: nullcat Date: Sat, 30 May 2026 17:10:47 +0800 Subject: [PATCH 19/22] fix(tsnet): add timeout handling for tsnet server initialization --- core/tsnet.go | 5 ++++- main.go | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/core/tsnet.go b/core/tsnet.go index ed2d18c..3e391b1 100644 --- a/core/tsnet.go +++ b/core/tsnet.go @@ -4,6 +4,7 @@ import ( "context" fmt2 "fmt" "log/slog" + "time" "tailscale.com/ipn" @@ -40,7 +41,9 @@ func InitTsNet(ctx context.Context, cfg *Core, logger *slog.Logger, withDebugLog return nil, err } - status, err := srv.Up(ctx) + cnclCtx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + status, err := srv.Up(cnclCtx) if err != nil { logger.With(slog.String("error", err.Error())).Error("bring up tsnet server failed") return nil, err diff --git a/main.go b/main.go index 9e38d7a..66e353e 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "errors" "flag" "log/slog" "os" @@ -21,9 +22,14 @@ func serviceLogic(configPath string, isTsnetDebug bool, logger *slog.Logger) boo } ctx, cancelAll := context.WithCancel(context.Background()) + defer cancelAll() logger.Info("initializing tsnet server") srv, err := core.InitTsNet(ctx, &cfg.Core, logger, isTsnetDebug) if err != nil { + if errors.Is(err, context.DeadlineExceeded) { + logger.Debug("tsnet initialization timed out, retrying") + return false + } logger.With( slog.String("error", err.Error())).Error("Error initializing tsnet") os.Exit(1) From da17faa6dc28d23212a2af188723048cda0e464f Mon Sep 17 00:00:00 2001 From: nullcat Date: Sat, 30 May 2026 17:11:44 +0800 Subject: [PATCH 20/22] fix(utils): adjust getPeerInfo position --- core/utils.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/core/utils.go b/core/utils.go index e6eabf7..7f84bab 100644 --- a/core/utils.go +++ b/core/utils.go @@ -110,12 +110,6 @@ func getPeerFromRules(ctx context.Context, srv *tsnet.Server, rules map[string][ func peerConnectivityLogic(ctx context.Context, lc *local.Client, relativePeers []netip.Addr, logger *slog.Logger) { for _, peer := range relativePeers { loLog := logger.With("peer", peer) - peerInfo, err := lc.WhoIs(ctx, peer.String()) - if err != nil { - loLog.Warn("failed to get peer info", "err", err) - } else { - loLog = loLog.With("name", peerInfo.Node.ComputedName) - } ping, err := func() (*ipnstate.PingResult, error) { cnclCtx, cancel := context.WithTimeout(ctx, 5*time.Second) @@ -134,6 +128,13 @@ func peerConnectivityLogic(ctx context.Context, lc *local.Client, relativePeers continue } + peerInfo, err := lc.WhoIs(ctx, peer.String()) + if err != nil { + loLog.Warn("failed to get peer info", "err", err) + } else { + loLog = loLog.With("name", peerInfo.Node.ComputedName) + } + var connect string if ping.DERPRegionCode == "" { connect = "direct" From 0a5fde792577e1068a87513f3d664b2e282b24b5 Mon Sep 17 00:00:00 2001 From: nullcat Date: Sat, 30 May 2026 17:27:47 +0800 Subject: [PATCH 21/22] chore(forward): adjust log level --- core/forwarder.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/forwarder.go b/core/forwarder.go index 3dfc4ad..e6667be 100644 --- a/core/forwarder.go +++ b/core/forwarder.go @@ -18,7 +18,7 @@ func StartForwarders(ctx context.Context, srv *tsnet.Server, rules map[string][] for _, rule := range rrs { rule := rule tag := tag - slog.Debug("starting forwarder", + slog.Info("starting forwarder", slog.String("tag", tag), slog.String("protocol", rule.Protocol), slog.Int("tailscale_port", rule.TailscalePort), @@ -43,7 +43,7 @@ func StartConnectors(ctx context.Context, srv *tsnet.Server, rules map[string][] if rule.LocalAddr != "" { args = append(args, slog.String("local_addr", rule.LocalAddr)) } - slog.Debug("starting connector", args...) + slog.Info("starting connector", args...) go runConnector(ctx, srv, rule, tag) } } @@ -94,7 +94,7 @@ func runTCPForwarder(ctx context.Context, srv *tsnet.Server, rule ForwardRule, l logger.Error("failed to listen", "error", err) return } - logger.Info("listening", slog.String("on", fmt.Sprintf("tailscale:%s:%d", ip.String(), rule.TailscalePort))) + logger.Debug("listening", slog.String("on", fmt.Sprintf("tailscale:%s:%d", ip.String(), rule.TailscalePort))) go func() { <-ctx.Done() From ea246896e1bbffda95fc7fad3ab10e2406ec311b Mon Sep 17 00:00:00 2001 From: nullcat Date: Sat, 30 May 2026 17:28:23 +0800 Subject: [PATCH 22/22] chore: add README.md --- README.md | 142 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..5667ec0 --- /dev/null +++ b/README.md @@ -0,0 +1,142 @@ +# tsnet_pure 使用说明 + +`tsnet_pure` 是一个基于 `tailscale.com/tsnet` 的轻量转发工具,支持: + +- `forward`:将 **Tailscale 入站** 请求转发到本机服务(你 -> 其他人)。 +- `connect`:将 **本机/LAN 入站** 请求转发到 Tailscale 目标(其他人 -> 你)。 + +支持 `tcp`、`udp`,并对 `minecraft` 协议提供局域网发现能力。 + +## 1. 环境要求 + +- Go 1.26+ +- 可用的 Tailscale `auth_key`(或 Headscale 对应密钥) + +## 2. 安装与构建 + +在项目根目录执行: + +```powershell +go mod tidy +go build -o tslink.exe . +``` + +如果你已有编译产物,也可以直接运行现成的 `tslink.exe`。 + +## 3. 配置文件 + +默认使用根目录 `config.toml`,可通过参数 `-c` 指定其他路径。 + +项目已提供 `config.example.toml`,可复制后修改: + +```powershell +Copy-Item .\config.example.toml .\config.toml +``` + +### 3.1 `[core]` 配置 + +```toml +[core] +auth_key = "" # 必填 +control_url = "https://controlplane.tailscale.com" # 官方控制面或 Headscale +hostname = "" # 留空时自动使用机器名 +ephemeral = true +accept_routes = true +``` + +- `auth_key`:Tailscale/Headscale 授权密钥。 +- `control_url`:默认是官方控制面,使用 Headscale 时改为你的实例地址。 +- `hostname`:节点名,为空则自动取系统主机名。 +- `ephemeral`:是否使用临时节点。 +- `accept_routes`:是否自动启用路由接收(`RouteAll`)。 + +### 3.2 `[[forward.]]` 规则(你 -> 其他人) + +```toml +[[forward.web]] +protocol = "tcp" +tailscale_port = 8080 +local_addr = "127.0.0.1:9090" +``` + +含义:监听本机 Tailscale IP 的 `8080`,转发到本地 `127.0.0.1:9090`。 + +字段说明: + +- `protocol`:`tcp` 或 `udp` +- `tailscale_port`:对 Tailnet 暴露端口 +- `local_addr`:本地目标地址(`host:port`) + +### 3.3 `[[connect.]]` 规则(其他人 -> 你) + +```toml +[[connect.web]] +protocol = "tcp" +local_port = 9000 +dst_addr = "any-client-in.ts.net:8080" +``` + +含义:监听本机 `9000`,流量转发到 Tailscale 目标 `any-client-in.ts.net:8080`。 + +字段说明: + +- `protocol`:`tcp`、`udp` 或 `minecraft` +- `local_port`:本地监听端口 +- `local_addr`:本地监听地址(可选,默认 `0.0.0.0`) +- `dst_addr`:Tailscale 目标地址(`host:port`) +- `lan_enable`:仅 `minecraft` 场景常用;不填时 `minecraft` 默认启用 +- `lan_motd`:Minecraft 局域网广播提示文案 + +Minecraft 示例: + +```toml +[[connect.minecraft]] +protocol = "minecraft" +local_port = 25565 +dst_addr = "any-client-in.ts.net:25566" +lan_enable = true +lan_motd = "Minecraft via Tailscale" +``` + +## 4. 启动方式 + +```powershell +.\tslink.exe -c config.toml +``` + +常用参数: + +- `-c`:配置文件路径,默认 `config.toml` +- `-level`:日志级别,默认 `info`(可用:`debug|info|warn|error`) +- `-json-format`:输出 JSON 日志 +- `-diagnose`:启用 tsnet debug 日志 + +示例: + +```powershell +.\tslink.exe -c .\config.toml -level debug -diagnose +``` + +## 5. 运行与退出 + +- 启动后程序会初始化 tsnet 并按配置启动所有 `forward/connect` 规则。 +- 按 `Ctrl + C` 可优雅退出。 +- 内置 watchdog 可能在异常时自动触发重启逻辑。 + +## 6. 常见问题 + +### 6.1 首次启动失败/无法入网 + +- 检查 `auth_key` 是否正确、是否过期。 +- 若使用 Headscale,确认 `control_url` 可访问且 TLS/证书配置正确。 + +### 6.2 端口无法访问 + +- 检查本地防火墙与目标服务是否真的在 `local_addr` 监听。 +- 确认 `dst_addr` 可在 Tailnet 内解析并连通。 +- 核对 `protocol` 是否与目标服务一致(`tcp/udp` 不可混用)。 + +### 6.3 日志排查建议 + +- 使用 `-level debug` 查看更详细转发日志。 +- 需要 tsnet 内部信息时加 `-diagnose`。