Merge remote-tracking branch 'origin/master' into sf
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
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@v6
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
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: |
|
||||
BINARY_NAME="tslink"
|
||||
if [ "${{ matrix.goos }}" = "windows" ]; then
|
||||
BINARY_NAME="${BINARY_NAME}.exe"
|
||||
fi
|
||||
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')
|
||||
uses: crazy-max/ghaction-upx@v4
|
||||
with:
|
||||
files: |
|
||||
release/tslink*
|
||||
args: "-9"
|
||||
|
||||
- name: Get current date
|
||||
id: date
|
||||
run: echo "date=$(date +'%y%m%d')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: tslink-dev${{ steps.date.outputs.date }}-${{ matrix.goos }}_${{ matrix.goarch }}
|
||||
path: release/
|
||||
@@ -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.
|
||||
@@ -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.<tag>]]` 规则(你 -> 其他人)
|
||||
|
||||
```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.<tag>]]` 规则(其他人 -> 你)
|
||||
|
||||
```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`。
|
||||
+138
-27
@@ -6,6 +6,7 @@ import (
|
||||
"io"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/netip"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -17,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),
|
||||
@@ -42,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)
|
||||
}
|
||||
}
|
||||
@@ -87,17 +88,13 @@ 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)
|
||||
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()
|
||||
@@ -177,32 +174,115 @@ 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
|
||||
}
|
||||
pc, err := srv.ListenPacket("udp", fmt.Sprintf("%s:%d", ip.String(), rule.TailscalePort))
|
||||
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)
|
||||
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 +290,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 +337,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 +389,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 +515,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)
|
||||
|
||||
+14
-5
@@ -4,20 +4,27 @@ import (
|
||||
"context"
|
||||
fmt2 "fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"tailscale.com/ipn"
|
||||
|
||||
"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...))
|
||||
},
|
||||
@@ -34,7 +41,9 @@ func InitTsNet(ctx context.Context, cfg *Core, logger *slog.Logger) (*tsnet.Serv
|
||||
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
|
||||
|
||||
+35
-6
@@ -11,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"tailscale.com/client/local"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tsnet"
|
||||
)
|
||||
@@ -86,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)
|
||||
@@ -109,19 +109,39 @@ 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)
|
||||
loLog := logger.With("peer", peer)
|
||||
|
||||
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)
|
||||
return
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
loLog.Warn("connectivity: peer ping timeout")
|
||||
} else {
|
||||
loLog.Warn("connectivity: failed to ping peer", "err", err)
|
||||
}
|
||||
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"
|
||||
} 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,
|
||||
)
|
||||
@@ -160,3 +180,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
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"log/slog"
|
||||
"os"
|
||||
@@ -11,7 +12,7 @@ import (
|
||||
"tslink/core"
|
||||
)
|
||||
|
||||
func serviceLogic(configPath, configURL string, logger *slog.Logger) bool {
|
||||
func serviceLogic(configPath string, isTsnetDebug bool, configURL string, logger *slog.Logger) bool {
|
||||
// Determine config source: URL takes priority, then file
|
||||
configSource := configPath
|
||||
if configURL != "" {
|
||||
@@ -28,9 +29,14 @@ func serviceLogic(configPath, configURL string, logger *slog.Logger) bool {
|
||||
}
|
||||
|
||||
ctx, cancelAll := context.WithCancel(context.Background())
|
||||
defer cancelAll()
|
||||
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 {
|
||||
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)
|
||||
@@ -52,7 +58,7 @@ func serviceLogic(configPath, configURL string, logger *slog.Logger) bool {
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
logger.Debug("stopping tsnet service")
|
||||
srv.Close()
|
||||
_ = srv.Close() // just ignore it
|
||||
}()
|
||||
|
||||
for {
|
||||
@@ -73,6 +79,7 @@ func serviceLogic(configPath, configURL 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")
|
||||
configURL := flag.String("config-url", core.DefaultConfigURL, "URL to fetch config from (default from build ldflags)")
|
||||
@@ -83,7 +90,7 @@ func main() {
|
||||
logger.Info("Starting tslink server", "level", *logLevel, "configPath", *configPath)
|
||||
|
||||
for {
|
||||
isStopped := serviceLogic(*configPath, *configURL, logger)
|
||||
isStopped := serviceLogic(*configPath, *showTsnetDebugLog, *configURL, logger)
|
||||
if !isStopped {
|
||||
logger.Warn("tslink server restart")
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user