1- //go:build linux || darwin || freebsd
1+ //// go:build linux || darwin || freebsd
22
33package headscale
44
55import (
6+ "context"
67 "errors"
78 "fmt"
8- "github.com/juanfont/headscale/hscontrol"
9- "github.com/juanfont/headscale/hscontrol/types"
10- "github.com/labstack/echo/v4"
11- "github.com/spf13/viper"
129 "net/http"
1310 "net/netip"
1411 "net/url"
12+ "os"
13+ "sync"
1514 "time"
15+
16+ "github.com/juanfont/headscale/hscontrol"
17+ "github.com/juanfont/headscale/hscontrol/types"
18+ "github.com/labstack/echo/v4"
19+ )
20+
21+ // ================= 全局变量区 =================
22+ // 必须放在函数外面,否则每次请求进来都是新的,无法管理状态
23+ var (
24+ hsApp * hscontrol.Headscale // Headscale 实例
25+ hsRunning bool // 运行状态标记
26+ hsMutex sync.Mutex // 线程锁,防止同时点击启动炸掉
27+ hsCancel context.CancelFunc // 用于停止服务的开关
28+ hsError string // 记录启动时的报错信息
1629)
1730
18- // headscaleConfig 定义了Headscale的配置结构
31+ // headscaleConfig 前端表单结构
1932type headscaleConfig struct {
20- ServerURL string `form:"server_url"` // 服务器URL,客户端将连接到的地址
21- ListenAddr string `form:"listen_addr"` // 服务器监听地址
22- MetricsListenAddr string `form:"metrics_listen_addr"` // metrics监听地址
23- GRPCListenAddr string `form:"grpc_listen_addr"` // gRPC监听地址
24- PrivateKeyPath string `form:"private_key_path"` // Noise私钥路径
25- IPv4Prefix string `form:"ipv4_prefix"` // IPv4地址分配范围
26- IPv6Prefix string `form:"ipv6_prefix"` // IPv6地址分配范围
27- BaseDomain string `form:"base_domain"` // MagicDNS基础域名
28- ACMEEmail string `form:"acme_email"` // ACME注册邮箱
29- TLSLetsEncryptHostname string `form:"tls_letsencrypt_hostname"` // Let's Encrypt主机名
30- TLSCertPath string `form:"tls_cert_path"` // TLS证书路径
31- TLSKeyPath string `form:"tls_key_path"` // TLS私钥路径
32- UnixSocket string `form:"unix_socket"` // Unix套接字路径
33- UnixSocketPermission int `form:"unix_socket_permission"` // Unix套接字权限
34- DatabaseType string `form:"database_type"` // 数据库类型
35- SqlitePath string `form:"sqlite_path"` // SQLite数据库路径
36- DisableCheckUpdates bool `form:"disable_check_updates"` // 是否禁用更新检查
33+ ServerURL string `form:"server_url"`
34+ ListenAddr string `form:"listen_addr"`
35+ MetricsListenAddr string `form:"metrics_listen_addr"`
36+ GRPCListenAddr string `form:"grpc_listen_addr"`
37+ PrivateKeyPath string `form:"private_key_path"`
38+ IPv4Prefix string `form:"ipv4_prefix"`
39+ IPv6Prefix string `form:"ipv6_prefix"`
40+ BaseDomain string `form:"base_domain"`
3741}
3842
39- //const confPath = "/etc/headscale/config.yaml"
40-
43+ // Index 是唯一的入口函数
4144func Index (c echo.Context ) error {
42- req := & headscaleConfig {}
43- if err := c .Bind (req ); err != nil {
44- return err
45- }
46- if err := c .Validate (req ); err != nil {
47- return err
48- }
49- switch c .Request ().Method {
50- case "POST" :
51- if c .QueryParam ("status" ) == "start" {
45+ // 获取请求动作类型
46+ action := c .QueryParam ("status" )
47+ method := c .Request ().Method
48+
49+ // 1. 处理 POST 请求 (控制指令:启动、停止、检查状态)
50+ if method == "POST" {
51+ hsMutex .Lock ()
52+ defer hsMutex .Unlock ()
53+
54+ switch action {
55+ case "start" :
56+ // 如果已经在运行,直接返回成功
57+ if hsRunning {
58+ return c .JSON (200 , map [string ]any {"success" : true , "message" : "Already running" })
59+ }
60+
61+ // 绑定参数
62+ req := & headscaleConfig {}
63+ if err := c .Bind (req ); err != nil {
64+ return c .JSON (400 , map [string ]any {"success" : false , "message" : "Invalid config" })
65+ }
66+
67+ // 转换配置
5268 cfg , err := loadServerConfig (req )
5369 if err != nil {
54- return err
70+ return c . JSON ( 400 , map [ string ] any { "success" : false , "message" : err . Error ()})
5571 }
72+
73+ // 初始化 Headscale
5674 app , err := hscontrol .NewHeadscale (cfg )
5775 if err != nil {
58- return err
76+ return c . JSON ( 500 , map [ string ] any { "success" : false , "message" : "Init failed: " + err . Error ()})
5977 }
60- if err = app .Serve (); err != nil && ! errors .Is (err , http .ErrServerClosed ) {
61- return err
78+
79+ // 创建用于停止的 Context
80+ ctx , cancel := context .WithCancel (context .Background ())
81+
82+ // 更新全局状态
83+ hsApp = app
84+ hsCancel = cancel
85+ hsRunning = true
86+ hsError = ""
87+
88+ // 【关键点】放入 goroutine 异步运行,防止阻塞 HTTP 请求
89+ go func () {
90+ // 注意:这里需要 Headscale 支持 Context 传入或者只是单纯运行
91+ // 目前 Headscale 的 Serve 通常是阻塞的
92+ // 如果需要支持优雅关闭,可能需要修改 Headscale 源码或使用其提供的 Shutdown 方法
93+ // 这里简单处理:启动服务
94+ if err := app .Serve (); err != nil && ! errors .Is (err , http .ErrServerClosed ) {
95+ hsMutex .Lock ()
96+ hsRunning = false
97+ hsError = err .Error ()
98+ hsMutex .Unlock ()
99+ fmt .Println ("Headscale stopped unexpectedly:" , err )
100+ } else {
101+ // 正常退出
102+ hsMutex .Lock ()
103+ hsRunning = false
104+ hsMutex .Unlock ()
105+ }
106+ }()
107+
108+ // 这里稍微利用 Context 做一个假装的生命周期管理(Headscale原生Serve如果不接受Context,强制停止比较麻烦)
109+ // 为了演示,我们保存了 cancel,实际停止时可能需要关闭 listener
110+ go func () {
111+ <- ctx .Done ()
112+ // 当 cancel 被调用时,尝试关闭 app (如果 app 暴露了 Shutdown)
113+ // hsApp.Shutdown()
114+ }()
115+
116+ return c .JSON (200 , map [string ]any {"success" : true , "message" : "Headscale started in background" })
117+
118+ case "stop" :
119+ if ! hsRunning {
120+ return c .JSON (200 , map [string ]any {"success" : false , "message" : "Not running" })
121+ }
122+
123+ // 触发停止
124+ if hsCancel != nil {
125+ hsCancel () // 触发 Context 取消
62126 }
127+
128+ // 注意:由于 Go 的 http.Server 需要显式 Shutdown,
129+ // 如果 headscale 库没暴露 Shutdown 方法,这里可能关不掉端口。
130+ // 假设 hscontrol 内部处理了关闭逻辑,或者你可能需要重启整个主进程。
131+
132+ hsRunning = false
133+ hsApp = nil
134+ return c .JSON (200 , map [string ]any {"success" : true , "message" : "Stop signal sent" })
135+
136+ case "check" : // 前端轮询用
137+ statusStr := "stopped"
138+ if hsRunning {
139+ statusStr = "running"
140+ }
141+ return c .JSON (200 , map [string ]any {
142+ "status" : statusStr ,
143+ "error" : hsError ,
144+ })
145+
146+ default :
147+ // 可以在这里处理保存配置逻辑
148+ // saveConfigToDisk(c)
149+ return c .JSON (200 , map [string ]any {"success" : true , "message" : "Config saved (mock)" })
63150 }
151+ }
64152
65- return c .JSON (200 , "success" )
66- //case "PUT":
67- // data, err := io.ReadAll(c.Request().Body)
68- // defer c.Request().Body.Close()
69- // if err != nil {
70- // return err
71- // }
72- // if err := os.WriteFile(confPath, data, 0644); err != nil {
73- // return err
74- // }
75- // return c.JSON(200, "success")
76- case "GET" :
77- //file, err := os.ReadFile(confPath)
78- //if err != nil {
79- // return err
80- //}
153+ // 2. 处理 GET 请求 (渲染页面)
154+ if method == "GET" {
155+ // 可以在这里读取配置文件回显
81156 return c .Render (http .StatusOK , "headscale.template" , map [string ]any {
82- //"headscaleConfig": string(file),
83- "headscaleEnable" : viper .Get ("enable.frps" ).(bool ),
157+ "headscaleEnable" : hsRunning ,
84158 })
85159 }
86160
87161 return echo .ErrMethodNotAllowed
88162}
89163
164+ // 辅助函数:配置转换
90165func loadServerConfig (c * headscaleConfig ) (* types.Config , error ) {
91166 prefix4 , err := netip .ParsePrefix (c .IPv4Prefix )
92167 if err != nil {
@@ -100,42 +175,41 @@ func loadServerConfig(c *headscaleConfig) (*types.Config, error) {
100175
101176 derpURL , _ := url .Parse ("https://controlplane.tailscale.com/derpmap/default" )
102177
178+ // 获取当前路径,防止权限问题
179+ cwd , _ := os .Getwd ()
180+
103181 return & types.Config {
104- ServerURL : c .ServerURL , // 客户端将连接到的服务器URL
105- Addr : c .ListenAddr , // 服务器监听地址
106- MetricsAddr : c .MetricsListenAddr , // metrics监听地址
107- GRPCAddr : c .GRPCListenAddr , // gRPC监听地址
108- GRPCAllowInsecure : false , // 是否允许不安全的gRPC连接
109- EphemeralNodeInactivityTimeout : 30 * time .Minute , // 临时节点不活动超时时间
110- PrefixV4 : & prefix4 , // IPv4地址分配范围
111- PrefixV6 : & prefix6 , // IPv6地址分配范围
112- IPAllocation : types .IPAllocationStrategySequential , // IP分配策略
113- NoisePrivateKeyPath : c .PrivateKeyPath , // Noise协议私钥路径
114- BaseDomain : c .BaseDomain , // MagicDNS基础域名
115- Log : types.LogConfig {}, // 日志配置
116- DisableUpdateCheck : true , // 禁用更新检查
182+ ServerURL : c .ServerURL ,
183+ Addr : c .ListenAddr ,
184+ MetricsAddr : c .MetricsListenAddr ,
185+ GRPCAddr : c .GRPCListenAddr ,
186+ GRPCAllowInsecure : true ,
187+ EphemeralNodeInactivityTimeout : 30 * time .Minute ,
188+ PrefixV4 : & prefix4 ,
189+ PrefixV6 : & prefix6 ,
190+ IPAllocation : types .IPAllocationStrategySequential ,
191+ NoisePrivateKeyPath : c .PrivateKeyPath ,
192+ BaseDomain : c .BaseDomain ,
193+ Log : types.LogConfig {Level : 1 }, // 简单的日志配置
194+ DisableUpdateCheck : true ,
117195 Database : types.DatabaseConfig {
118- Type : "sqlite" , // 数据库类型
119- Debug : false , // 是否启用数据库调试
196+ Type : "sqlite3" ,
197+ Debug : false ,
120198 Sqlite : types.SqliteConfig {
121- Path : "/var/lib/ headscale/db.sqlite " , // SQLite数据库路径
122- WriteAheadLog : false , // 是否启用预写日志
199+ Path : fmt . Sprintf ( "%s/ headscale.db " , cwd ), // 改为当前目录
200+ WriteAheadLog : true ,
123201 },
124202 },
125203 DERP : types.DERPConfig {
126- ServerEnabled : false , // 是否启用DERP服务器
127- AutomaticallyAddEmbeddedDerpRegion : false , // 是否自动添加嵌入式DERP区域
128- URLs : []url.URL {* derpURL }, // DERP服务器URL列表
129- AutoUpdate : true , // 是否自动更新DERP地图
130- UpdateFrequency : 24 * time .Hour , // DERP地图更新频率
204+ ServerEnabled : false ,
205+ AutomaticallyAddEmbeddedDerpRegion : false ,
206+ URLs : []url.URL {* derpURL },
207+ AutoUpdate : true ,
208+ UpdateFrequency : 24 * time .Hour ,
131209 },
132-
133- TLS : types.TLSConfig {}, // TLS配置
134- ACMEURL : "https://acme-v02.api.letsencrypt.org/directory" , // ACME服务器URL
135- ACMEEmail : "" , // ACME注册邮箱
136- DNSConfig : types.DNSConfig {}, // DNS配置
137- //DNSUserNameInMagicDNS: false, // 是否在MagicDNS中使用用户名
138- UnixSocket : "/var/run/headscale.sock" , // Unix套接字路径
139- UnixSocketPermission : 0600 , // Unix套接字权限
210+ TLS : types.TLSConfig {},
211+ DNSConfig : types.DNSConfig {},
212+ UnixSocket : fmt .Sprintf ("%s/headscale.sock" , cwd ), // 改为当前目录
213+ UnixSocketPermission : 0755 ,
140214 }, nil
141215}
0 commit comments