@@ -0,0 +1,342 @@
//go:build windows
package nodeagent
import (
"context"
"errors"
"fmt"
"log/slog"
"net"
"net/netip"
"net/url"
"time"
"remlink/internal/bootstrap"
"remlink/internal/control"
"remlink/internal/identity"
"remlink/internal/localization"
"remlink/internal/logging"
"remlink/internal/model"
"remlink/internal/overlay/clientwg"
windowsplatform "remlink/internal/platform/windows"
"remlink/internal/platform/windows/dpapi"
"remlink/internal/platform/windows/netinfo"
"remlink/internal/platform/windows/route"
"remlink/internal/protocol"
sessionruntime "remlink/internal/session"
netstackgateway "remlink/internal/subnetgateway/netstack"
)
// Run starts one Windows Node and rebuilds all network-derived state whenever
// the Server requires a new public Bootstrap configuration.
func Run ( ctx context . Context , options Options ) error {
if err := options . validate ( ) ; err != nil {
return err
}
bootstrapLogger := nodeLogger ( options , logging . ModuleBootstrap )
for {
err := runOnce ( ctx , options )
if ctx . Err ( ) != nil || errors . Is ( err , context . Canceled ) {
return nil
}
if ! errors . Is ( err , protocol . ErrRebootstrapRequired ) {
return err
}
if bootstrapLogger != nil {
bootstrapLogger . Info ( "正在根据公网 Bootstrap 配置重建节点网络运行时" )
}
}
}
func runOnce ( ctx context . Context , options Options ) error {
bootstrapLogger := nodeLogger ( options , logging . ModuleBootstrap )
wgLogger := nodeLogger ( options , logging . ModuleWG )
controlLogger := nodeLogger ( options , logging . ModuleControl )
sessionLogger := nodeLogger ( options , logging . ModuleSession )
routeLogger := nodeLogger ( options , logging . ModuleRoute )
identityStore , err := identity . NewStore ( options . IdentityPath , dpapi . Protector { } )
if err != nil {
return err
}
bootstrapClient , err := bootstrap . NewClient ( options . ServerURL , nil )
if err != nil {
return err
}
nodeIdentity , network , err := bootstrap . Enroll ( ctx , identityStore , bootstrapClient , bootstrap . EnrollConfig {
NodeType : options . NodeType , NodeName : options . NodeName , ServerURL : options . ServerURL ,
JoinToken : options . JoinToken , Version : options . Version , OSVersion : bootstrap . CurrentOSVersion ( ) ,
} )
if err != nil {
return err
}
if bootstrapLogger != nil {
bootstrapLogger . Info ( "已收到 Server 下发的权威 Bootstrap 配置" , "node_id" , nodeIdentity . NodeID , "config_version" , network . ConfigVersion )
}
overlayCIDR , _ := netip . ParsePrefix ( network . OverlayCIDR )
localPrefixes , err := netinfo . DirectIPv4Prefixes ( windowsplatform . AdapterName )
if err != nil {
return err
}
if conflict , found := netinfo . FindConflict ( overlayCIDR , localPrefixes ) ; found {
return fmt . Errorf ( "%s: Overlay 网段 %s 与本地网络 %s 冲突" , protocol . ErrorOverlayLocalConflict , overlayCIDR , conflict )
}
nodeIP , _ := netip . ParseAddr ( network . OverlayIP )
adapter , err := windowsplatform . OpenRemLink ( windowsplatform . AdapterConfig {
Address : netip . PrefixFrom ( nodeIP , overlayCIDR . Bits ( ) ) , MTU : network . MTU ,
} )
if err != nil {
return err
}
ownedByWireGuard := false
defer func ( ) {
if ! ownedByWireGuard {
_ = adapter . Close ( )
}
} ( )
wireGuard , err := clientwg . NewFromAdapter ( adapter , wgLogger )
if err != nil {
return err
}
ownedByWireGuard = true
defer wireGuard . Close ( )
privateKey , err := clientwg . ParseKeyBase64 ( nodeIdentity . PrivateKey . String ( ) )
if err != nil {
return err
}
serverPublicKey , err := clientwg . ParseKeyBase64 ( network . ServerWGPublicKey )
if err != nil {
return err
}
if err := wireGuard . Configure ( clientwg . Config {
PrivateKey : privateKey , ServerPublicKey : serverPublicKey ,
ServerEndpoint : network . ServerWGEndpoint , OverlayAllowedIPs : [ ] netip . Prefix { overlayCIDR } ,
PersistentKeepalive : 25 * time . Second ,
} ) ; err != nil {
return err
}
if err := wireGuard . Up ( ) ; err != nil {
return err
}
if err := waitForOverlayControl ( ctx , network . ControlURL ) ; err != nil {
return err
}
if wgLogger != nil {
wgLogger . Info ( "节点 Overlay 网络已就绪" , "node_id" , nodeIdentity . NodeID , "overlay_ip" , nodeIP , "server" , network . ServerWGEndpoint )
}
if options . OnOverlayReady != nil {
options . OnOverlayReady ( nodeIP )
}
routeManager , err := route . NewManager ( adapter . LUID ( ) , overlayCIDR , identityStore )
if err != nil {
return err
}
capabilities := options . Capabilities
tcpFlowLimit := options . TCPFlowLimit
if tcpFlowLimit <= 0 {
tcpFlowLimit = netstackgateway . DefaultTCPFlowLimit
}
udpFlowLimit := options . UDPFlowLimit
if udpFlowLimit <= 0 {
udpFlowLimit = netstackgateway . DefaultUDPFlowLimit
}
udpIdleTimeout := options . UDPIdleTimeout
if udpIdleTimeout <= 0 {
udpIdleTimeout = netstackgateway . DefaultUDPIdleTimeout
}
if options . NodeType == model . NodeTypeSite {
capabilities = protocol . NodeCapabilities {
RemoteSubnet : true , NetstackStatus : "netstack" ,
TCPCapacity : tcpFlowLimit , UDPCapacity : udpFlowLimit ,
}
}
var handleEnvelope func ( context . Context , protocol . ControlEnvelope ) error
onPacketReject := func ( rejectErr error , source netip . Addr ) {
if options . ApplicationLogger != nil {
_ , _ = options . ApplicationLogger . SampleSecurityWarning ( context . Background ( ) , logging . ModuleSubnet , "session-datagram-rejected" , "会话数据报已被拒绝" ,
slog . String ( "source_overlay_ip" , source . String ( ) ) , slog . String ( "reason" , rejectErr . Error ( ) ) )
}
}
onPacketDrop := func ( event clientwg . DropEvent ) {
if options . ApplicationLogger != nil {
_ , _ = options . ApplicationLogger . SampleSecurityWarning ( context . Background ( ) , logging . ModuleTUN , string ( event . Reason ) , "PacketMux 已丢弃数据包" ,
slog . String ( "reason" , string ( event . Reason ) ) , slog . String ( "destination" , event . Destination . String ( ) ) )
}
}
var controlClient * control . Client
controlClient , err = control . NewClient ( control . ClientConfig {
URL : network . ControlURL ,
OnConnectionState : options . OnControlState ,
OnHeartbeatRTT : options . OnLatency ,
Hello : protocol . HelloPayload {
NodeID : nodeIdentity . NodeID , NodeToken : nodeIdentity . NodeToken ,
ConfigVersion : nodeIdentity . ConfigVersion , Capabilities : capabilities ,
OSVersion : bootstrap . CurrentOSVersion ( ) , Version : options . Version ,
} ,
} , func ( messageContext context . Context , envelope protocol . ControlEnvelope ) error {
if controlLogger != nil {
controlLogger . Info ( "收到 Control 控制消息" , "type" , envelope . Type , "request_id" , envelope . RequestID )
}
if envelope . Type == protocol . ControlRebootstrapRequired {
response , configErr := bootstrapClient . Config ( messageContext , bootstrap . ConfigRequest {
NodeID : nodeIdentity . NodeID , NodeToken : nodeIdentity . NodeToken ,
} )
if configErr == nil {
configErr = bootstrap . ValidateNetworkConfig ( response . Network )
}
if configErr == nil {
nextOverlay , _ := netip . ParsePrefix ( response . Network . OverlayCIDR )
prefixes , prefixErr := netinfo . DirectIPv4Prefixes ( windowsplatform . AdapterName )
if prefixErr != nil {
return prefixErr
}
if conflict , found := netinfo . FindConflict ( nextOverlay , prefixes ) ; found {
if controlLogger != nil {
controlLogger . Error ( "节点拒绝 Overlay 网段迁移" , "error_code" , protocol . ErrorOverlayLocalConflict ,
"overlay_cidr" , nextOverlay , "local_prefix" , conflict )
}
reportContext , cancelReport := context . WithTimeout ( messageContext , 3 * time . Second )
reportErr := controlClient . Send ( reportContext , protocol . ControlHeartbeat ,
fmt . Sprintf ( "overlay-conflict-%d" , time . Now ( ) . UnixNano ( ) ) , protocol . HeartbeatPayload {
Timestamp : time . Now ( ) . UTC ( ) , Status : string ( protocol . ErrorOverlayLocalConflict ) ,
} )
cancelReport ( )
if reportErr != nil {
return fmt . Errorf ( "report %s: %v: %w" , protocol . ErrorOverlayLocalConflict , reportErr , protocol . ErrRebootstrapRequired )
}
// Keep the old Overlay active long enough for the Server to receive
// the status. The migration will close this old Control path.
return nil
}
}
// A public Bootstrap refresh is also the fallback when preflight
// could not be completed over the old Control path.
return protocol . ErrRebootstrapRequired
}
if handleEnvelope == nil {
return errors . New ( "节点会话运行时尚未初始化" )
}
return handleEnvelope ( messageContext , envelope )
} )
if err != nil {
return err
}
var closeRuntime func ( ) error
switch options . NodeType {
case model . NodeTypeEngineer :
engineerRuntime , err := sessionruntime . NewEngineerRuntime ( ctx , sessionruntime . EngineerConfig {
LocalOverlayIP : nodeIP , OverlayCIDR : overlayCIDR , UDPPort : network . SessionUDPPort ,
Routes : routeManager , Device : wireGuard . MuxTun ( ) , Control : controlClient ,
OnPacketReject : onPacketReject , OnPacketDrop : onPacketDrop ,
OnNodeList : options . OnNodeList ,
OnSession : func ( status model . SessionStatus , sessionID uint64 , reason string ) {
if sessionLogger != nil {
sessionLogger . Info ( "Engineer 会话状态变更" , "status" , localization . SessionStatus ( string ( status ) ) , "session_id" , sessionID , "reason" , localization . Reason ( reason ) )
}
if options . OnSession != nil {
options . OnSession ( status , sessionID , reason )
}
} ,
} )
if err != nil {
return err
}
handleEnvelope = engineerRuntime . HandleControl
closeRuntime = engineerRuntime . Close
if options . OnEngineerReady != nil {
options . OnEngineerReady ( engineerRuntime )
}
case model . NodeTypeSite :
var siteRuntime * sessionruntime . SiteRuntime
gateway , err := netstackgateway . New ( netstackgateway . Config {
MTU : network . MTU , TCPFlowLimit : tcpFlowLimit ,
UDPFlowLimit : udpFlowLimit , UDPIdleTimeout : udpIdleTimeout ,
Egress : func ( packetContext context . Context , sessionID uint64 , packet [ ] byte ) error {
if siteRuntime == nil {
return errors . New ( "Site 会话运行时尚未就绪" )
}
return siteRuntime . Egress ( packetContext , sessionID , packet )
} ,
} )
if err != nil {
return fmt . Errorf ( "初始化 Site netstack 网关失败:%w" , err )
}
siteRuntime , err = sessionruntime . NewSiteRuntime ( ctx , sessionruntime . SiteConfig {
LocalOverlayIP : nodeIP , OverlayCIDR : overlayCIDR , UDPPort : network . SessionUDPPort ,
TCPFlowLimit : tcpFlowLimit , UDPFlowLimit : udpFlowLimit ,
Routes : routeManager , Gateway : gateway , Control : controlClient ,
OnPacketReject : onPacketReject ,
OnRoute : func ( sessionID uint64 , prefix netip . Prefix , result route . LookupResult ) {
if routeLogger != nil {
routeLogger . Info ( "Site 路由查询" , "session_id" , sessionID , "cidr" , prefix , "result" , localization . RouteResult ( string ( result ) ) )
}
if options . OnRoute != nil {
options . OnRoute ( sessionID , prefix , string ( result ) )
}
} ,
OnSession : func ( status model . SessionStatus , sessionID uint64 , reason string ) {
if sessionLogger != nil {
sessionLogger . Info ( "Site 会话状态变更" , "status" , localization . SessionStatus ( string ( status ) ) , "session_id" , sessionID , "reason" , localization . Reason ( reason ) )
}
if options . OnSession != nil {
options . OnSession ( status , sessionID , reason )
}
} ,
} )
if err != nil {
_ = gateway . Close ( )
return err
}
handleEnvelope = siteRuntime . HandleControl
closeRuntime = siteRuntime . Close
if options . OnSiteReady != nil {
options . OnSiteReady ( )
}
default :
return errors . New ( "不支持的节点类型" )
}
defer func ( ) {
_ = closeRuntime ( )
if options . OnEngineerReady != nil && options . NodeType == model . NodeTypeEngineer {
options . OnEngineerReady ( nil )
}
} ( )
err = controlClient . Run ( ctx )
return err
}
func nodeLogger ( options Options , module logging . Module ) * slog . Logger {
if options . ApplicationLogger != nil {
logger , err := options . ApplicationLogger . For ( module )
if err == nil {
return logger
}
}
return options . Logger
}
func waitForOverlayControl ( ctx context . Context , controlURL string ) error {
parsed , err := url . Parse ( controlURL )
if err != nil {
return err
}
waitContext , cancel := context . WithTimeout ( ctx , 30 * time . Second )
defer cancel ( )
dialer := & net . Dialer { Timeout : 3 * time . Second }
var lastErr error
for {
connection , err := dialer . DialContext ( waitContext , "tcp4" , parsed . Host )
if err == nil {
return connection . Close ( )
}
lastErr = err
if ctx . Err ( ) != nil {
return ctx . Err ( )
}
select {
case <- waitContext . Done ( ) :
return fmt . Errorf ( "Overlay Control endpoint is unreachable: %v: %w" , lastErr , protocol . ErrRebootstrapRequired )
case <- time . After ( time . Second ) :
}
}
}