From 62037742e3458b53168dd57494851cbbbe99994f Mon Sep 17 00:00:00 2001 From: atavism Date: Tue, 30 Sep 2025 09:15:25 -0700 Subject: [PATCH 01/11] add start endpoint --- vpn/ipc/endpoints.go | 1 + vpn/ipc/server.go | 35 ++++++++++++++++++++++++++--------- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/vpn/ipc/endpoints.go b/vpn/ipc/endpoints.go index 7f972f36..9acd43a0 100644 --- a/vpn/ipc/endpoints.go +++ b/vpn/ipc/endpoints.go @@ -3,6 +3,7 @@ package ipc const ( statusEndpoint = "/status" metricsEndpoint = "/metrics" + startServiceEndpoint = "/service/start" closeServiceEndpoint = "/service/close" groupsEndpoint = "/groups" selectEndpoint = "/outbound/select" diff --git a/vpn/ipc/server.go b/vpn/ipc/server.go index 2e186413..39fec06b 100644 --- a/vpn/ipc/server.go +++ b/vpn/ipc/server.go @@ -5,6 +5,7 @@ package ipc import ( "context" + "encoding/json" "errors" "fmt" "log/slog" @@ -13,8 +14,6 @@ import ( "github.com/go-chi/chi/v5" "github.com/sagernet/sing-box/experimental/clashapi" - - "github.com/getlantern/radiance/traces" ) var ( @@ -36,7 +35,8 @@ type Server struct { svr *http.Server service Service - router chi.Router + router chi.Router + startFn func(context.Context, string, string) error } // NewServer creates a new Server instance with the provided Service. @@ -55,6 +55,7 @@ func NewServer(service Service) *Server { s.router.Post(selectEndpoint, s.selectHandler) s.router.Get(clashModeEndpoint, s.clashModeHandler) s.router.Post(clashModeEndpoint, s.clashModeHandler) + s.router.Post(startServiceEndpoint, s.startServiceHandler) s.router.Post(closeServiceEndpoint, s.closeServiceHandler) s.router.Post(closeConnectionsEndpoint, s.closeConnectionHandler) return s @@ -95,6 +96,28 @@ func CloseService(ctx context.Context) error { return err } +func StartService(ctx context.Context, group, tag string) error { + _, err := sendRequest[empty](ctx, "POST", startServiceEndpoint, selection{GroupTag: group, OutboundTag: tag}) + return err +} + +func (s *Server) startServiceHandler(w http.ResponseWriter, r *http.Request) { + if s.startFn == nil { + http.Error(w, "start not supported", http.StatusNotImplemented) + return + } + var p selection + if err := json.NewDecoder(r.Body).Decode(&p); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if err := s.startFn(r.Context(), p.GroupTag, p.OutboundTag); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) +} + func (s *Server) closeServiceHandler(w http.ResponseWriter, r *http.Request) { service := s.service s.service = &closedService{} @@ -106,12 +129,6 @@ func (s *Server) closeServiceHandler(w http.ResponseWriter, r *http.Request) { if flusher, ok := w.(http.Flusher); ok { flusher.Flush() } - - go func() { - if err := s.Close(); err != nil { - traces.RecordError(context.Background(), err) - } - }() } // closedService is a stub service that always returns "closed" status. It's used to replace the From 4681415052cf6084a7f22d1f96a62601548d11f7 Mon Sep 17 00:00:00 2001 From: atavism Date: Tue, 30 Sep 2025 09:31:56 -0700 Subject: [PATCH 02/11] clean-ups --- vpn/ipc/server.go | 21 +++++++++++++++++---- vpn/tunnel.go | 15 +-------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/vpn/ipc/server.go b/vpn/ipc/server.go index 39fec06b..0ba4a98d 100644 --- a/vpn/ipc/server.go +++ b/vpn/ipc/server.go @@ -29,6 +29,8 @@ type Service interface { Close() error } +type StartFn func(ctx context.Context, group, tag string) (Service, error) + // Server represents the IPC server that communicates over a Unix domain socket for Unix-like // systems, and a named pipe for Windows. type Server struct { @@ -36,7 +38,7 @@ type Server struct { service Service router chi.Router - startFn func(context.Context, string, string) error + startFn StartFn } // NewServer creates a new Server instance with the provided Service. @@ -90,31 +92,42 @@ func (s *Server) Close() error { return s.svr.Close() } -// CloseService sends a request to shutdown the service. This will also close the IPC server. +// CloseService sends a request to shutdown the service func CloseService(ctx context.Context) error { _, err := sendRequest[empty](ctx, "POST", closeServiceEndpoint, nil) return err } +// StartService sends a request to start the service func StartService(ctx context.Context, group, tag string) error { _, err := sendRequest[empty](ctx, "POST", startServiceEndpoint, selection{GroupTag: group, OutboundTag: tag}) return err } +func (s *Server) SetStartFn(fn StartFn) { s.startFn = fn } +func (s *Server) SetService(svc Service) { s.service = svc } + func (s *Server) startServiceHandler(w http.ResponseWriter, r *http.Request) { if s.startFn == nil { http.Error(w, "start not supported", http.StatusNotImplemented) return } + // check if service is already running + if s.service.Status() == StatusRunning { + w.WriteHeader(http.StatusOK) + return + } var p selection if err := json.NewDecoder(r.Body).Decode(&p); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } - if err := s.startFn(r.Context(), p.GroupTag, p.OutboundTag); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + svc, err := s.startFn(r.Context(), p.GroupTag, p.OutboundTag) + if err != nil { + http.Error(w, err.Error(), http.StatusServiceUnavailable) return } + s.SetService(svc) w.WriteHeader(http.StatusOK) } diff --git a/vpn/tunnel.go b/vpn/tunnel.go index 453ac98b..5db5cb0c 100644 --- a/vpn/tunnel.go +++ b/vpn/tunnel.go @@ -75,19 +75,6 @@ func establishConnection(group, tag string, opts O.Options, dataPath string, pla t := &tunnel{} t.status.Store(ipc.StatusInitializing) - ipcServer = ipc.NewServer(t) - if err := ipcServer.Start(common.DataPath()); err != nil { - slog.Error("Failed to start IPC server", "error", err) - return fmt.Errorf("starting IPC server: %w", err) - } - slog.Debug("IPC server started") - defer func() { - if err != nil { - t.status.Store(ipc.StatusClosed) - ipcServer.Close() - } - }() - t.ctx, t.cancel = context.WithCancel(sbx.BoxContext()) if err := t.init(opts, dataPath, platIfce); err != nil { slog.Error("Failed to initialize tunnel", "error", err) @@ -289,7 +276,7 @@ func (t *tunnel) Close() error { } func (t *tunnel) close() error { - return errors.Join(t.Close(), ipcServer.Close()) + return t.Close() } func (t *tunnel) Ctx() context.Context { From 88ec392944335a7e3e7587449b05cba0cba213b8 Mon Sep 17 00:00:00 2001 From: atavism Date: Tue, 30 Sep 2025 09:40:47 -0700 Subject: [PATCH 03/11] Add InitIPC --- vpn/ipc.go | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 vpn/ipc.go diff --git a/vpn/ipc.go b/vpn/ipc.go new file mode 100644 index 00000000..f2016ede --- /dev/null +++ b/vpn/ipc.go @@ -0,0 +1,53 @@ +package vpn + +import ( + "context" + "fmt" + + "github.com/getlantern/radiance/common" + "github.com/getlantern/radiance/vpn/ipc" + "github.com/sagernet/sing-box/experimental/clashapi" + "github.com/sagernet/sing-box/experimental/libbox" +) + +var platIfceProvider func() libbox.PlatformInterface + +// closedSvc is a stub service used while the tunnel is down +type closedSvc struct{} + +func (closedSvc) Ctx() context.Context { return context.Background() } +func (closedSvc) Status() string { return ipc.StatusClosed } +func (closedSvc) ClashServer() *clashapi.Server { return nil } +func (closedSvc) Close() error { return nil } + +// InitIPC starts the long-lived IPC server and hooks it up to establishConnection +func InitIPC(basePath string, provider func() libbox.PlatformInterface) error { + if ipcServer != nil { + // already started + return nil + } + platIfceProvider = provider + ipcServer = ipc.NewServer(closedSvc{}) + // start tunnel via IPC. How /service/start brings the tunnel up + ipcServer.SetStartFn(func(ctx context.Context, group, tag string) (ipc.Service, error) { + path := common.DataPath() + + _ = newSplitTunnel(path) + + opts, err := buildOptions(group, path) + if err != nil { + return nil, fmt.Errorf("build options: %w", err) + } + + var pi libbox.PlatformInterface + if platIfceProvider != nil { + pi = platIfceProvider() + } + + if err := establishConnection(group, tag, opts, path, pi); err != nil { + return nil, err + } + return tInstance, nil + }) + return ipcServer.Start(basePath) +} From 0265b6dcfeb157903bf5c2c373f556f561a5f71a Mon Sep 17 00:00:00 2001 From: atavism Date: Tue, 30 Sep 2025 09:41:54 -0700 Subject: [PATCH 04/11] use basePath --- vpn/ipc.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/vpn/ipc.go b/vpn/ipc.go index f2016ede..88e3957a 100644 --- a/vpn/ipc.go +++ b/vpn/ipc.go @@ -30,7 +30,10 @@ func InitIPC(basePath string, provider func() libbox.PlatformInterface) error { ipcServer = ipc.NewServer(closedSvc{}) // start tunnel via IPC. How /service/start brings the tunnel up ipcServer.SetStartFn(func(ctx context.Context, group, tag string) (ipc.Service, error) { - path := common.DataPath() + path := basePath + if path == "" { + path = common.DataPath() + } _ = newSplitTunnel(path) From d2be5294c3849ece9f5d3e133406565c80643a0c Mon Sep 17 00:00:00 2001 From: atavism Date: Tue, 30 Sep 2025 09:58:22 -0700 Subject: [PATCH 05/11] Add comments --- vpn/ipc.go | 5 +++++ vpn/ipc/server.go | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/vpn/ipc.go b/vpn/ipc.go index 88e3957a..7d86edc1 100644 --- a/vpn/ipc.go +++ b/vpn/ipc.go @@ -3,6 +3,7 @@ package vpn import ( "context" "fmt" + "runtime" "github.com/getlantern/radiance/common" "github.com/getlantern/radiance/vpn/ipc" @@ -27,6 +28,10 @@ func InitIPC(basePath string, provider func() libbox.PlatformInterface) error { return nil } platIfceProvider = provider + if runtime.GOOS != "windows" && basePath != "" { + ipc.SetSocketPath(basePath) + } + ipcServer = ipc.NewServer(closedSvc{}) // start tunnel via IPC. How /service/start brings the tunnel up ipcServer.SetStartFn(func(ctx context.Context, group, tag string) (ipc.Service, error) { diff --git a/vpn/ipc/server.go b/vpn/ipc/server.go index 0ba4a98d..4458562c 100644 --- a/vpn/ipc/server.go +++ b/vpn/ipc/server.go @@ -104,7 +104,11 @@ func StartService(ctx context.Context, group, tag string) error { return err } -func (s *Server) SetStartFn(fn StartFn) { s.startFn = fn } +// SetStartFn registers a function that will be called when the start endpoint is hit +func (s *Server) SetStartFn(fn StartFn) { s.startFn = fn } + +// SetService updates the service attached to the server. +// Typically called when starting or replacing the VPN tunnel func (s *Server) SetService(svc Service) { s.service = svc } func (s *Server) startServiceHandler(w http.ResponseWriter, r *http.Request) { From c9c750cfaf35736a817d523feaac8d0d26d0756c Mon Sep 17 00:00:00 2001 From: atavism Date: Tue, 30 Sep 2025 12:50:43 -0700 Subject: [PATCH 06/11] code review updates --- vpn/ipc/server.go | 3 +++ vpn/tunnel.go | 8 ++++---- vpn/tunnel_test.go | 6 +++--- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/vpn/ipc/server.go b/vpn/ipc/server.go index 4458562c..099762d8 100644 --- a/vpn/ipc/server.go +++ b/vpn/ipc/server.go @@ -115,6 +115,9 @@ func (s *Server) startServiceHandler(w http.ResponseWriter, r *http.Request) { if s.startFn == nil { http.Error(w, "start not supported", http.StatusNotImplemented) return + } else if s.service == nil { + http.Error(w, "service not attached", http.StatusInternalServerError) + return } // check if service is already running if s.service.Status() == StatusRunning { diff --git a/vpn/tunnel.go b/vpn/tunnel.go index 5db5cb0c..fa271dc6 100644 --- a/vpn/tunnel.go +++ b/vpn/tunnel.go @@ -103,6 +103,10 @@ func establishConnection(group, tag string, opts O.Options, dataPath string, pla tInstance = t t.status.Store(ipc.StatusRunning) + // If the IPC server is already running, make sure it points to the live tunnel + if ipcServer != nil { + ipcServer.SetService(t) + } return nil } @@ -275,10 +279,6 @@ func (t *tunnel) Close() error { return err } -func (t *tunnel) close() error { - return t.Close() -} - func (t *tunnel) Ctx() context.Context { return t.ctx } diff --git a/vpn/tunnel_test.go b/vpn/tunnel_test.go index bc586e76..d325f319 100644 --- a/vpn/tunnel_test.go +++ b/vpn/tunnel_test.go @@ -26,7 +26,7 @@ func TestEstablishConnection(t *testing.T) { testEstablishConnection(t, *tOpts) tun := tInstance - assert.NoError(t, tun.close(), "failed to close lbService") + assert.NoError(t, tun.Close(), "failed to close lbService") assert.Equal(t, ipc.StatusClosed, tun.Status(), "tun should be closed") } @@ -63,7 +63,7 @@ func TestUpdateServers(t *testing.T) { testEstablishConnection(t, *testOpts) tun := tInstance defer func() { - tun.close() + tun.Close() }() time.Sleep(500 * time.Millisecond) @@ -130,7 +130,7 @@ func testEstablishConnection(t *testing.T, opts sbO.Options) { require.NoError(t, err, "failed to establish connection") t.Cleanup(func() { if tInstance != nil { - tInstance.close() + tInstance.Close() } }) From a238ce408e287514492b1568dc75a887cb8fc3e3 Mon Sep 17 00:00:00 2001 From: atavism Date: Tue, 30 Sep 2025 17:33:48 -0700 Subject: [PATCH 07/11] clean-up --- vpn/ipc/server.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/vpn/ipc/server.go b/vpn/ipc/server.go index 099762d8..243bd0ea 100644 --- a/vpn/ipc/server.go +++ b/vpn/ipc/server.go @@ -115,12 +115,9 @@ func (s *Server) startServiceHandler(w http.ResponseWriter, r *http.Request) { if s.startFn == nil { http.Error(w, "start not supported", http.StatusNotImplemented) return - } else if s.service == nil { - http.Error(w, "service not attached", http.StatusInternalServerError) - return } // check if service is already running - if s.service.Status() == StatusRunning { + if s.service != nil && s.service.Status() == StatusRunning { w.WriteHeader(http.StatusOK) return } From f9a3ff31997073e151d25a17f2d2248c3381f7d2 Mon Sep 17 00:00:00 2001 From: atavism Date: Mon, 6 Oct 2025 04:37:05 -0700 Subject: [PATCH 08/11] code review updates --- vpn/ipc/conn_windows.go | 86 +++++++++++++++++++++-------------------- vpn/ipc/endpoints.go | 1 + vpn/ipc/server.go | 32 +++++++++++++++ vpn/tunnel.go | 9 +++++ 4 files changed, 86 insertions(+), 42 deletions(-) diff --git a/vpn/ipc/conn_windows.go b/vpn/ipc/conn_windows.go index 18bae4c3..5610defc 100644 --- a/vpn/ipc/conn_windows.go +++ b/vpn/ipc/conn_windows.go @@ -8,6 +8,7 @@ import ( "context" "fmt" "net" + "syscall" "time" "github.com/Microsoft/go-winio" @@ -20,9 +21,7 @@ const ( apiURL = "http://pipe" connectTimeout = 10 * time.Second - // TODO: I don't know which one should be used - sddl = `O:BAG:BAD:PAI(A;OICI;GWGR;;;BU)(A;OICI;GWGR;;;SY)` - // sddl = `D:P(A;;GA;;;SY)(A;;GA;;;BA)(A;;GA;;;BU)` + sddl = `D:P(A;;GA;;;SY)(A;;GRGW;;;IU)(A;;GRGW;;;BA)` ) // SetSocketPath not supported on Windows. @@ -56,7 +55,19 @@ func listen(_ string) (net.Listener, error) { // is needed to call Windows API functions that require a handle. type winioConn interface { net.Conn - FD() uintptr + SyscallConn() (syscall.RawConn, error) +} + +func withConnHandle(c winioConn, fn func(h windows.Handle) error) error { + rc, err := c.SyscallConn() + if err != nil { + return err + } + var callErr error + if err := rc.Control(func(fd uintptr) { callErr = fn(windows.Handle(fd)) }); err != nil { + return err + } + return callErr } type winioListener struct { @@ -86,7 +97,7 @@ func (l *winioListener) Accept() (conn net.Conn, err error) { } defer pipeToken.Close() - procToken, err := getProcessToken(wc) + procToken, err := getServerProcessToken() if err != nil { return nil, fmt.Errorf("failed to get process token: %w", err) } @@ -103,67 +114,58 @@ func (l *winioListener) Accept() (conn net.Conn, err error) { return wc, nil } +func tokenUserSID(t windows.Token) (*windows.SID, error) { + u, err := t.GetTokenUser() + if err != nil { + return nil, fmt.Errorf("failed to get token user: %w", err) + } + return u.User.Sid, nil +} + // verifySameUser checks if two tokens belong to the same user. func verifySameUser(t1, t2 windows.Token) (bool, error) { - u1, err := t1.GetTokenUser() + s1, err := tokenUserSID(t1) if err != nil { return false, fmt.Errorf("failed to get token user: %w", err) } - u2, err := t2.GetTokenUser() + s2, err := tokenUserSID(t2) if err != nil { return false, fmt.Errorf("failed to get token user: %w", err) } - return u1.User.Sid.Equals(u2.User.Sid), nil + return s1.Equals(s2), nil } // getPipeClientToken retrieves the impersonation token for the pipe client. func getPipeClientToken(conn winioConn) (windows.Token, error) { - ph := windows.Handle(conn.FD()) - if ph == 0 { - return 0, fmt.Errorf("invalid pipe handle") - } - - err := impersonateNamedPipeClient(ph) - if err != nil { - return 0, fmt.Errorf("failed to impersonate client: %w", err) - } - defer windows.RevertToSelf() - var token windows.Token - err = windows.OpenThreadToken(windows.CurrentThread(), windows.TOKEN_DUPLICATE|windows.TOKEN_QUERY, true, &token) - if err != nil { - return 0, fmt.Errorf("failed to open thread token: %w", err) + if err := withConnHandle(conn, func(h windows.Handle) error { + err := impersonateNamedPipeClient(h) + if err != nil { + return fmt.Errorf("failed to impersonate client: %w", err) + } + defer windows.RevertToSelf() + + return windows.OpenThreadToken(windows.CurrentThread(), windows.TOKEN_DUPLICATE|windows.TOKEN_QUERY, true, &token) + }); err != nil { + return 0, err } return token, nil } -// getProcessToken retrieves the process token for the pipe client. -func getProcessToken(pc winioConn) (windows.Token, error) { - pid, err := getPipeClientPID(pc) - if err != nil { - return 0, fmt.Errorf("failed to get client process id: %w", err) - } - h, err := windows.OpenProcess(windows.PROCESS_QUERY_INFORMATION, false, pid) - if err != nil { - return 0, fmt.Errorf("failed to open process token: %w", err) - } - defer windows.CloseHandle(h) - +// getServerProcessToken retrieves the process token for the pipe client. +func getServerProcessToken() (windows.Token, error) { var token windows.Token - if err := windows.OpenProcessToken(h, windows.TOKEN_QUERY, &token); err != nil { - return 0, fmt.Errorf("failed to open process token: %w", err) + if err := windows.OpenProcessToken(windows.CurrentProcess(), windows.TOKEN_QUERY, &token); err != nil { + return 0, fmt.Errorf("failed to open service process token: %w", err) } return token, nil } func getPipeClientPID(pc winioConn) (uint32, error) { - ph := windows.Handle(pc.FD()) - if ph == 0 { - return 0, fmt.Errorf("invalid pipe handle") - } var pid uint32 - err := windows.GetNamedPipeClientProcessId(ph, &pid) - if err != nil { + if err := withConnHandle(pc, func(h windows.Handle) error { + return windows.GetNamedPipeClientProcessId(h, &pid) + }); err != nil { return 0, fmt.Errorf("failed to get client process id: %w", err) } return pid, nil diff --git a/vpn/ipc/endpoints.go b/vpn/ipc/endpoints.go index 9acd43a0..721bf5ae 100644 --- a/vpn/ipc/endpoints.go +++ b/vpn/ipc/endpoints.go @@ -4,6 +4,7 @@ const ( statusEndpoint = "/status" metricsEndpoint = "/metrics" startServiceEndpoint = "/service/start" + stopServiceEndpoint = "/service/stop" closeServiceEndpoint = "/service/close" groupsEndpoint = "/groups" selectEndpoint = "/outbound/select" diff --git a/vpn/ipc/server.go b/vpn/ipc/server.go index 243bd0ea..8ca6a71c 100644 --- a/vpn/ipc/server.go +++ b/vpn/ipc/server.go @@ -12,6 +12,7 @@ import ( "net/http" "time" + "github.com/getlantern/radiance/traces" "github.com/go-chi/chi/v5" "github.com/sagernet/sing-box/experimental/clashapi" ) @@ -58,6 +59,7 @@ func NewServer(service Service) *Server { s.router.Get(clashModeEndpoint, s.clashModeHandler) s.router.Post(clashModeEndpoint, s.clashModeHandler) s.router.Post(startServiceEndpoint, s.startServiceHandler) + s.router.Post(stopServiceEndpoint, s.stopServiceHandler) s.router.Post(closeServiceEndpoint, s.closeServiceHandler) s.router.Post(closeConnectionsEndpoint, s.closeConnectionHandler) return s @@ -104,6 +106,12 @@ func StartService(ctx context.Context, group, tag string) error { return err } +// StopService sends a request to stop the service (IPC server stays up) +func StopService(ctx context.Context) error { + _, err := sendRequest[empty](ctx, "POST", stopServiceEndpoint, nil) + return err +} + // SetStartFn registers a function that will be called when the start endpoint is hit func (s *Server) SetStartFn(fn StartFn) { s.startFn = fn } @@ -135,6 +143,22 @@ func (s *Server) startServiceHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } +func (s *Server) stopServiceHandler(w http.ResponseWriter, r *http.Request) { + svc := s.service + s.service = &closedService{} + + if svc != nil { + if err := svc.Close(); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } + w.WriteHeader(http.StatusOK) + if f, ok := w.(http.Flusher); ok { + f.Flush() + } +} + func (s *Server) closeServiceHandler(w http.ResponseWriter, r *http.Request) { service := s.service s.service = &closedService{} @@ -146,6 +170,14 @@ func (s *Server) closeServiceHandler(w http.ResponseWriter, r *http.Request) { if flusher, ok := w.(http.Flusher); ok { flusher.Flush() } + + go func() { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := s.svr.Shutdown(ctx); err != nil && !errors.Is(err, http.ErrServerClosed) { + traces.RecordError(context.Background(), err) + } + }() } // closedService is a stub service that always returns "closed" status. It's used to replace the diff --git a/vpn/tunnel.go b/vpn/tunnel.go index fa271dc6..c6509368 100644 --- a/vpn/tunnel.go +++ b/vpn/tunnel.go @@ -106,7 +106,16 @@ func establishConnection(group, tag string, opts O.Options, dataPath string, pla // If the IPC server is already running, make sure it points to the live tunnel if ipcServer != nil { ipcServer.SetService(t) + return nil + } + // fallback: start IPC server here for platforms that don't call InitIPC yet + ipcServer = ipc.NewServer(t) + if err := ipcServer.Start(dataPath); err != nil { + slog.Error("Failed to start IPC server: %w", err) + t.status.Store(ipc.StatusClosed) + return fmt.Errorf("starting IPC server: %w", err) } + slog.Debug("IPC server started") return nil } From 7e114b1aa6c884dda68335342fd510fdcd465985 Mon Sep 17 00:00:00 2001 From: atavism Date: Mon, 6 Oct 2025 04:49:20 -0700 Subject: [PATCH 09/11] update slog.Error call to fix tests --- vpn/tunnel.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vpn/tunnel.go b/vpn/tunnel.go index c6509368..4edf88db 100644 --- a/vpn/tunnel.go +++ b/vpn/tunnel.go @@ -111,7 +111,7 @@ func establishConnection(group, tag string, opts O.Options, dataPath string, pla // fallback: start IPC server here for platforms that don't call InitIPC yet ipcServer = ipc.NewServer(t) if err := ipcServer.Start(dataPath); err != nil { - slog.Error("Failed to start IPC server: %w", err) + slog.Error("Failed to start IPC server", "error", err) t.status.Store(ipc.StatusClosed) return fmt.Errorf("starting IPC server: %w", err) } From b56c0204828db2e391465b11ece7ac4782afcda8 Mon Sep 17 00:00:00 2001 From: atavism Date: Mon, 6 Oct 2025 05:00:06 -0700 Subject: [PATCH 10/11] Remove unused, add comments --- vpn/ipc/conn_windows.go | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/vpn/ipc/conn_windows.go b/vpn/ipc/conn_windows.go index 5610defc..0eb2e880 100644 --- a/vpn/ipc/conn_windows.go +++ b/vpn/ipc/conn_windows.go @@ -51,13 +51,14 @@ func listen(_ string) (net.Listener, error) { return &winioListener{ln}, nil } -// winioConn is an helper interface to access the underlying file descriptor of a winio.Conn. This -// is needed to call Windows API functions that require a handle. +// winioConn is an helper interface that exposes the standard syscall.Conn so we can +// access the underlying handle via RawConn.Control type winioConn interface { net.Conn SyscallConn() (syscall.RawConn, error) } +// withConnHandle runs the function with the connection’s HANDLE pinned by the runtime func withConnHandle(c winioConn, fn func(h windows.Handle) error) error { rc, err := c.SyscallConn() if err != nil { @@ -160,13 +161,3 @@ func getServerProcessToken() (windows.Token, error) { } return token, nil } - -func getPipeClientPID(pc winioConn) (uint32, error) { - var pid uint32 - if err := withConnHandle(pc, func(h windows.Handle) error { - return windows.GetNamedPipeClientProcessId(h, &pid) - }); err != nil { - return 0, fmt.Errorf("failed to get client process id: %w", err) - } - return pid, nil -} From b7ccaecf59094927819a6a41f8d942c68ba9a9b4 Mon Sep 17 00:00:00 2001 From: atavism Date: Mon, 6 Oct 2025 05:00:12 -0700 Subject: [PATCH 11/11] Remove unused, add comments --- vpn/ipc/conn_windows.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vpn/ipc/conn_windows.go b/vpn/ipc/conn_windows.go index 0eb2e880..07abfdda 100644 --- a/vpn/ipc/conn_windows.go +++ b/vpn/ipc/conn_windows.go @@ -58,7 +58,7 @@ type winioConn interface { SyscallConn() (syscall.RawConn, error) } -// withConnHandle runs the function with the connection’s HANDLE pinned by the runtime +// withConnHandle runs the function with the connection’s handle pinned by the runtime func withConnHandle(c winioConn, fn func(h windows.Handle) error) error { rc, err := c.SyscallConn() if err != nil {