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"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -17,7 +18,7 @@ func StartForwarders(ctx context.Context, srv *tsnet.Server, rules map[string][]
|
|||||||
for _, rule := range rrs {
|
for _, rule := range rrs {
|
||||||
rule := rule
|
rule := rule
|
||||||
tag := tag
|
tag := tag
|
||||||
slog.Debug("starting forwarder",
|
slog.Info("starting forwarder",
|
||||||
slog.String("tag", tag),
|
slog.String("tag", tag),
|
||||||
slog.String("protocol", rule.Protocol),
|
slog.String("protocol", rule.Protocol),
|
||||||
slog.Int("tailscale_port", rule.TailscalePort),
|
slog.Int("tailscale_port", rule.TailscalePort),
|
||||||
@@ -42,7 +43,7 @@ func StartConnectors(ctx context.Context, srv *tsnet.Server, rules map[string][]
|
|||||||
if rule.LocalAddr != "" {
|
if rule.LocalAddr != "" {
|
||||||
args = append(args, slog.String("local_addr", 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)
|
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) {
|
func runTCPForwarder(ctx context.Context, srv *tsnet.Server, rule ForwardRule, logger *slog.Logger) {
|
||||||
ip4, ip6 := srv.TailscaleIPs()
|
ip := getSelfTsnetAddr(srv)
|
||||||
ip := ip4
|
|
||||||
if !ip.IsValid() {
|
|
||||||
ip = ip6
|
|
||||||
}
|
|
||||||
ln, err := srv.Listen("tcp", fmt.Sprintf("%s:%d", ip.String(), rule.TailscalePort))
|
ln, err := srv.Listen("tcp", fmt.Sprintf("%s:%d", ip.String(), rule.TailscalePort))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("failed to listen", "error", err)
|
logger.Error("failed to listen", "error", err)
|
||||||
return
|
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() {
|
go func() {
|
||||||
<-ctx.Done()
|
<-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) {
|
func runUDPForwarder(ctx context.Context, srv *tsnet.Server, rule ForwardRule, logger *slog.Logger) {
|
||||||
ip4, ip6 := srv.TailscaleIPs()
|
ip := getSelfTsnetAddr(srv)
|
||||||
ip := ip4
|
ln, err := srv.Listen("udp", fmt.Sprintf("%s:%d", ip.String(), rule.TailscalePort))
|
||||||
if !ip.IsValid() {
|
|
||||||
ip = ip6
|
|
||||||
}
|
|
||||||
pc, err := srv.ListenPacket("udp", fmt.Sprintf("%s:%d", ip.String(), rule.TailscalePort))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("failed to listen", "error", err)
|
logger.Error("failed to listen", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
logger.Info("listening", slog.String("on", fmt.Sprintf("tailscale:%s:%d", ip.String(), rule.TailscalePort)))
|
logger.Info("listening", slog.String("on", fmt.Sprintf("tailscale:%s:%d", ip.String(), rule.TailscalePort)))
|
||||||
|
|
||||||
relay := &udpRelay{
|
go func() {
|
||||||
listenConn: pc,
|
<-ctx.Done()
|
||||||
dialAddr: rule.LocalAddr,
|
ln.Close()
|
||||||
logger: logger,
|
}()
|
||||||
direction: "local",
|
|
||||||
sessions: make(map[string]*udpSession),
|
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 {
|
type udpSession struct {
|
||||||
conn net.Conn
|
conn net.Conn
|
||||||
remote net.Addr
|
remote net.Addr
|
||||||
lastUse time.Time
|
inTailscale bool
|
||||||
|
lastUse time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
type udpRelay struct {
|
type udpRelay struct {
|
||||||
@@ -210,6 +290,8 @@ type udpRelay struct {
|
|||||||
dialAddr string
|
dialAddr string
|
||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
direction string
|
direction string
|
||||||
|
srv *tsnet.Server
|
||||||
|
ctx context.Context
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
sessions map[string]*udpSession
|
sessions map[string]*udpSession
|
||||||
@@ -255,23 +337,43 @@ func (r *udpRelay) run(ctx context.Context) {
|
|||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
sess, exists := r.sessions[key]
|
sess, exists := r.sessions[key]
|
||||||
if !exists {
|
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 {
|
if err != nil {
|
||||||
r.mu.Unlock()
|
r.mu.Unlock()
|
||||||
r.logger.Error("failed to dial", "error", err)
|
r.logger.Error("failed to dial", "error", err)
|
||||||
continue
|
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.sessions[key] = sess
|
||||||
r.mu.Unlock()
|
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)
|
go r.readSession(key, sess)
|
||||||
} else {
|
} else {
|
||||||
sess.lastUse = time.Now()
|
sess.lastUse = time.Now()
|
||||||
r.mu.Unlock()
|
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 {
|
if _, err := sess.conn.Write(buf[:n]); err != nil {
|
||||||
r.logger.Error("failed to write", "error", err)
|
r.logger.Error("failed to write", "error", err)
|
||||||
r.removeSession(key)
|
r.removeSession(key)
|
||||||
@@ -287,6 +389,13 @@ func (r *udpRelay) readSession(key string, sess *udpSession) {
|
|||||||
r.removeSession(key)
|
r.removeSession(key)
|
||||||
return
|
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 {
|
if _, err := r.listenConn.WriteTo(buf[:n], sess.remote); err != nil {
|
||||||
r.logger.Error("failed to write back", "error", err)
|
r.logger.Error("failed to write back", "error", err)
|
||||||
r.removeSession(key)
|
r.removeSession(key)
|
||||||
@@ -406,6 +515,8 @@ func runUDPConnector(ctx context.Context, srv *tsnet.Server, rule ConnectRule, l
|
|||||||
dialAddr: rule.DstAddr,
|
dialAddr: rule.DstAddr,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
direction: "tailscale",
|
direction: "tailscale",
|
||||||
|
srv: srv,
|
||||||
|
ctx: ctx,
|
||||||
sessions: make(map[string]*udpSession),
|
sessions: make(map[string]*udpSession),
|
||||||
}
|
}
|
||||||
relay.run(ctx)
|
relay.run(ctx)
|
||||||
|
|||||||
+14
-5
@@ -4,20 +4,27 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
fmt2 "fmt"
|
fmt2 "fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"time"
|
||||||
|
|
||||||
"tailscale.com/ipn"
|
"tailscale.com/ipn"
|
||||||
|
|
||||||
"tailscale.com/tsnet"
|
"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{
|
srv := &tsnet.Server{
|
||||||
Hostname: "tslink-" + cfg.Hostname,
|
Hostname: "tslink-" + cfg.Hostname,
|
||||||
AuthKey: cfg.AuthKey,
|
AuthKey: cfg.AuthKey,
|
||||||
Ephemeral: cfg.Ephemeral,
|
Ephemeral: cfg.Ephemeral,
|
||||||
Logf: func(fmt string, args ...interface{}) {
|
Logf: dbgLogger,
|
||||||
logger.With(slog.String("from", "tsnet")).Debug(fmt2.Sprintf(fmt, args...))
|
|
||||||
},
|
|
||||||
UserLogf: func(fmt string, args ...interface{}) {
|
UserLogf: func(fmt string, args ...interface{}) {
|
||||||
logger.With(slog.String("from", "tsnet")).Info(fmt2.Sprintf(fmt, args...))
|
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
|
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 {
|
if err != nil {
|
||||||
logger.With(slog.String("error", err.Error())).Error("bring up tsnet server failed")
|
logger.With(slog.String("error", err.Error())).Error("bring up tsnet server failed")
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
+35
-6
@@ -11,6 +11,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"tailscale.com/client/local"
|
"tailscale.com/client/local"
|
||||||
|
"tailscale.com/ipn/ipnstate"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/tsnet"
|
"tailscale.com/tsnet"
|
||||||
)
|
)
|
||||||
@@ -86,7 +87,6 @@ func getPeerFromRules(ctx context.Context, srv *tsnet.Server, rules map[string][
|
|||||||
ap, _, err := net.SplitHostPort(rule.DstAddr)
|
ap, _, err := net.SplitHostPort(rule.DstAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Debug("error parsing rule", "tag", tag, "dst", rule.DstAddr, "err", err)
|
logger.Debug("error parsing rule", "tag", tag, "dst", rule.DstAddr, "err", err)
|
||||||
// err log
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
addr, err := resolveAddr(ctx, srv, ap)
|
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) {
|
func peerConnectivityLogic(ctx context.Context, lc *local.Client, relativePeers []netip.Addr, logger *slog.Logger) {
|
||||||
for _, peer := range relativePeers {
|
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 {
|
if err != nil {
|
||||||
logger.Debug("connectivity: failed to ping peer", "peer", peer, "err", err)
|
if errors.Is(err, context.DeadlineExceeded) {
|
||||||
return
|
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
|
var connect string
|
||||||
if ping.DERPRegionCode == "" {
|
if ping.DERPRegionCode == "" {
|
||||||
connect = "direct"
|
connect = "direct"
|
||||||
} else {
|
} else {
|
||||||
connect = ping.DERPRegionCode
|
connect = ping.DERPRegionCode
|
||||||
}
|
}
|
||||||
logger.Info("connectivity: peer pinged",
|
loLog.Info("connectivity: peer pinged",
|
||||||
"peer", peer,
|
|
||||||
"latency", fmt.Sprintf("%.2fms", ping.LatencySeconds*1000),
|
"latency", fmt.Sprintf("%.2fms", ping.LatencySeconds*1000),
|
||||||
"connect", connect,
|
"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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
@@ -11,7 +12,7 @@ import (
|
|||||||
"tslink/core"
|
"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
|
// Determine config source: URL takes priority, then file
|
||||||
configSource := configPath
|
configSource := configPath
|
||||||
if configURL != "" {
|
if configURL != "" {
|
||||||
@@ -28,9 +29,14 @@ func serviceLogic(configPath, configURL string, logger *slog.Logger) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancelAll := context.WithCancel(context.Background())
|
ctx, cancelAll := context.WithCancel(context.Background())
|
||||||
|
defer cancelAll()
|
||||||
logger.Info("initializing tsnet server")
|
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 err != nil {
|
||||||
|
if errors.Is(err, context.DeadlineExceeded) {
|
||||||
|
logger.Debug("tsnet initialization timed out, retrying")
|
||||||
|
return false
|
||||||
|
}
|
||||||
logger.With(
|
logger.With(
|
||||||
slog.String("error", err.Error())).Error("Error initializing tsnet")
|
slog.String("error", err.Error())).Error("Error initializing tsnet")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -52,7 +58,7 @@ func serviceLogic(configPath, configURL string, logger *slog.Logger) bool {
|
|||||||
go func() {
|
go func() {
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
logger.Debug("stopping tsnet service")
|
logger.Debug("stopping tsnet service")
|
||||||
srv.Close()
|
_ = srv.Close() // just ignore it
|
||||||
}()
|
}()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
@@ -73,6 +79,7 @@ func serviceLogic(configPath, configURL string, logger *slog.Logger) bool {
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
useJsonFormatLogger := flag.Bool("json-format", false, "use json format logger")
|
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)")
|
logLevel := flag.String("level", "info", "log level (DEBUG|INFO|WARN|ERROR)")
|
||||||
configPath := flag.String("c", "config.toml", "path to config file")
|
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)")
|
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)
|
logger.Info("Starting tslink server", "level", *logLevel, "configPath", *configPath)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
isStopped := serviceLogic(*configPath, *configURL, logger)
|
isStopped := serviceLogic(*configPath, *showTsnetDebugLog, *configURL, logger)
|
||||||
if !isStopped {
|
if !isStopped {
|
||||||
logger.Warn("tslink server restart")
|
logger.Warn("tslink server restart")
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user