package tunnel
import (
"bytes"
"fmt"
"log"
"os"
"runtime"
"time"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/svc"
"golang.org/x/sys/windows/svc/mgr"
"golang.zx2c4.com/wireguard/windows/conf"
"golang.zx2c4.com/wireguard/windows/driver"
"golang.zx2c4.com/wireguard/windows/elevate"
"golang.zx2c4.com/wireguard/windows/ringlogger"
"golang.zx2c4.com/wireguard/windows/services"
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
)
type tunnelService struct {
Path string
}
func (service *tunnelService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (svcSpecificEC bool, exitCode uint32) {
serviceState := svc.StartPending
changes <- svc.Status{State: serviceState}
var watcher *interfaceWatcher
var adapter *driver.Adapter
var luid winipcfg.LUID
var config *conf.Config
var err error
serviceError := services.ErrorSuccess
defer func() {
svcSpecificEC, exitCode = services.DetermineErrorCode(err, serviceError)
logErr := services.CombineErrors(err, serviceError)
if logErr != nil {
log.Println(logErr)
}
serviceState = svc.StopPending
changes <- svc.Status{State: serviceState}
stopIt := make(chan bool, 1)
go func() {
t := time.NewTicker(time.Second * 30)
for {
select {
case <-t.C:
t.Stop()
buf := make([]byte, 1024)
for {
n := runtime.Stack(buf, true)
if n < len(buf) {
buf = buf[:n]
break
}
buf = make([]byte, 2*len(buf))
}
lines := bytes.Split(buf, []byte{'\n'})
log.Println("Failed to shutdown after 30 seconds. Probably dead locked. Printing stack and killing.")
for _, line := range lines {
if len(bytes.TrimSpace(line)) > 0 {
log.Println(string(line))
}
}
os.Exit(777)
return
case <-stopIt:
t.Stop()
return
}
}
}()
if logErr == nil && adapter != nil && config != nil {
logErr = runScriptCommand(config.Interface.PreDown, config.Name)
}
if watcher != nil {
watcher.Destroy()
}
if adapter != nil {
adapter.Close()
}
if logErr == nil && adapter != nil && config != nil {
_ = runScriptCommand(config.Interface.PostDown, config.Name)
}
stopIt <- true
log.Println("Shutting down")
}()
var logFile string
logFile, err = conf.LogFile(true)
if err != nil {
serviceError = services.ErrorRingloggerOpen
return
}
err = ringlogger.InitGlobalLogger(logFile, "TUN")
if err != nil {
serviceError = services.ErrorRingloggerOpen
return
}
config, err = conf.LoadFromPath(service.Path)
if err != nil {
serviceError = services.ErrorLoadConfiguration
return
}
config.DeduplicateNetworkEntries()
log.SetPrefix(fmt.Sprintf("[%s] ", config.Name))
services.PrintStarting()
if services.StartedAtBoot() {
if m, err := mgr.Connect(); err == nil {
if lockStatus, err := m.LockStatus(); err == nil && lockStatus.IsLocked {
/* If we don't do this, then the driver installation will block forever, because
* installing a network adapter starts the driver service too. Apparently at boot time,
* Windows 8.1 locks the SCM for each service start, creating a deadlock if we don't
* announce that we're running before starting additional services.
*/
log.Printf("SCM locked for %v by %s, marking service as started", lockStatus.Age, lockStatus.Owner)
serviceState = svc.Running
changes <- svc.Status{State: serviceState}
}
m.Disconnect()
}
}
evaluateStaticPitfalls()
log.Println("Watching network interfaces")
watcher, err = watchInterface()
if err != nil {
serviceError = services.ErrorSetNetConfig
return
}
log.Println("Resolving DNS names")
err = config.ResolveEndpoints()
if err != nil {
serviceError = services.ErrorDNSLookup
return
}
log.Println("Creating network adapter")
for i := 0; i < 15; i++ {
if i > 0 {
time.Sleep(time.Second)
log.Printf("Retrying adapter creation after failure because system just booted (T+%v): %v", windows.DurationSinceBoot(), err)
}
adapter, err = driver.CreateAdapter(config.Name, "WireGuard", deterministicGUID(config))
if err == nil || !services.StartedAtBoot() {
break
}
}
if err != nil {
err = fmt.Errorf("Error creating adapter: %w", err)
serviceError = services.ErrorCreateNetworkAdapter
return
}
luid = adapter.LUID()
driverVersion, err := driver.RunningVersion()
if err != nil {
log.Printf("Warning: unable to determine driver version: %v", err)
} else {
log.Printf("Using WireGuardNT/%d.%d", (driverVersion>>16)&0xffff, driverVersion&0xffff)
}
err = adapter.SetLogging(driver.AdapterLogOn)
if err != nil {
err = fmt.Errorf("Error enabling adapter logging: %w", err)
serviceError = services.ErrorCreateNetworkAdapter
return
}
err = runScriptCommand(config.Interface.PreUp, config.Name)
if err != nil {
serviceError = services.ErrorRunScript
return
}
err = enableFirewall(config, luid)
if err != nil {
serviceError = services.ErrorFirewall
return
}
log.Println("Dropping privileges")
err = elevate.DropAllPrivileges(true)
if err != nil {
serviceError = services.ErrorDropPrivileges
return
}
log.Println("Setting interface configuration")
err = adapter.SetConfiguration(config.ToDriverConfiguration())
if err != nil {
serviceError = services.ErrorDeviceSetConfig
return
}
err = adapter.SetAdapterState(driver.AdapterStateUp)
if err != nil {
serviceError = services.ErrorDeviceBringUp
return
}
watcher.Configure(adapter, config, luid)
err = runScriptCommand(config.Interface.PostUp, config.Name)
if err != nil {
serviceError = services.ErrorRunScript
return
}
changes <- svc.Status{State: serviceState, Accepts: svc.AcceptStop | svc.AcceptShutdown}
var started bool
for {
select {
case c := <-r:
switch c.Cmd {
case svc.Stop, svc.Shutdown:
return
case svc.Interrogate:
changes <- c.CurrentStatus
default:
log.Printf("Unexpected service control request #%d\n", c)
}
case <-watcher.started:
if !started {
serviceState = svc.Running
changes <- svc.Status{State: serviceState, Accepts: svc.AcceptStop | svc.AcceptShutdown}
log.Println("Startup complete")
started = true
}
case e := <-watcher.errors:
serviceError, err = e.serviceError, e.err
return
}
}
}
func Run(confPath string) error {
name, err := conf.NameFromPath(confPath)
if err != nil {
return err
}
serviceName, err := conf.ServiceNameOfTunnel(name)
if err != nil {
return err
}
return svc.Run(serviceName, &tunnelService{confPath})
}
这段Go代码实现了一个在Windows服务环境中运行的隧道服务,主要用于管理和配置WireGuard隧道。它处理了服务的启动、配置、网络适配器的创建、权限的降低以及服务的停止和监控等多个方面。以下是对代码的详细分析:
代码结构与功能
1. tunnelService
结构体
type tunnelService struct {
Path string
}
tunnelService
结构体只有一个字段Path
,用于存储配置文件的路径。这个结构体实现了svc.Handler
接口,用于响应Windows服务控制管理器(SCM)的请求。
2. Execute
方法
func (service *tunnelService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (svcSpecificEC bool, exitCode uint32) {
- 这是
tunnelService
结构体的核心方法,用于响应服务控制管理器(SCM)的请求,处理服务的启动和停止逻辑。
初始化与服务状态报告
- 服务启动时,首先将状态设置为
StartPending
,并通过changes
通道报告状态给SCM。 - 初始化了一些关键变量,例如
watcher
(网络接口监视器)、adapter
(网络适配器)、luid
(本地接口标识符)和config
(隧道配置)。
延迟处理函数
defer
语句在函数结束时执行,用于清理资源和处理可能的错误。这包括:- 确保服务在退出时进入
StopPending
状态。 - 检测可能的死锁情况,打印堆栈跟踪并终止程序。
- 在适配器关闭之前,运行配置中的
PreDown
和PostDown
脚本命令。
- 确保服务在退出时进入
日志初始化
- 使用
ringlogger.InitGlobalLogger
初始化全局日志记录器,用于记录隧道服务的所有日志输出。
加载配置并处理启动逻辑
- 加载配置文件,处理网络接口配置和DNS解析。
- 创建网络适配器,并在系统刚启动时处理适配器创建可能失败的情况。
- 在适配器创建成功后,设置适配器的配置并将其状态设置为“启动”。
降低权限
- 通过
elevate.DropAllPrivileges(true)
减少服务的权限,以降低潜在的安全风险。
运行脚本和配置防火墙
- 在适配器启动前后分别运行
PreUp
和PostUp
脚本,以确保配置的生效。 - 配置防火墙规则,确保隧道流量的安全。
服务状态更新与监控
- 服务进入运行状态
svc.Running
后,通过changes
通道通知SCM,接受停止和关机请求。 - 使用
watcher
监控网络接口的状态变化,并根据变化调整服务状态。
3. Run
函数
func Run(confPath string) error {
Run
函数用于启动tunnelService
服务。它根据配置文件路径生成服务名称,并调用svc.Run
来启动服务。
总结与分析
关键功能与设计理念
-
服务管理: 代码实现了一个典型的Windows服务,通过与SCM交互,管理服务的启动、停止和运行状态。这使得隧道服务能够在Windows系统启动时自动运行,并在系统关机时正常停止。
-
网络适配器管理: 代码负责创建和配置WireGuard网络适配器,包括处理适配器的日志记录、配置设置和状态管理等。这部分确保了隧道服务能够正确地在网络适配器上执行,并保证流量的正确路由。
-
权限管理: 在服务启动后,代码会降低服务的权限,从而减少安全风险。权限降低是在完成所有需要高权限操作之后进行的,这是一个安全的设计。
-
错误处理与日志记录: 代码在多个关键点使用了错误处理,并通过全局日志记录器记录错误信息。这使得服务在运行过程中更加稳健,便于调试和问题诊断。
-
脚本执行: 在服务启动和关闭过程中,代码会执行配置中的脚本(例如
PreUp
、PostUp
等),这些脚本用于执行额外的配置操作,如配置路由或防火墙规则。 -
安全考虑: 代码通过降低权限、监控服务状态、防止死锁等措施来增强服务的安全性和可靠性。
总体而言,这段代码设计严谨,功能齐全,适合在Windows环境中作为系统服务运行,提供安全稳定的WireGuard隧道服务。