mirror of
https://github.com/go-gost/x.git
synced 2026-10-19 16:16:32 +00:00
plugin: manage external plugin subprocess lifecycle
Add Command []string to PluginConfig and a new x/plugin package that spawns plugin subprocesses (deduplicated by argv) and terminates them on Shutdown. Linux uses Pdeathsig, Windows a kill-on-close job object, so children follow an abrupt parent exit. Wire Spawn into every config/parsing plugin constructor.
This commit is contained in:
@@ -148,6 +148,7 @@ type TLSOptions struct {
|
||||
type PluginConfig struct {
|
||||
Type string `json:"type"`
|
||||
Addr string `json:"addr"`
|
||||
Command []string `yaml:"command,omitempty" json:"command,omitempty"`
|
||||
TLS *TLSConfig `yaml:",omitempty" json:"tls,omitempty"`
|
||||
Timeout time.Duration `yaml:",omitempty" json:"timeout,omitempty"`
|
||||
Token string `yaml:",omitempty" json:"token,omitempty"`
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/go-gost/x/config"
|
||||
"github.com/go-gost/x/internal/loader"
|
||||
"github.com/go-gost/x/internal/plugin"
|
||||
xplugin "github.com/go-gost/x/plugin"
|
||||
"github.com/go-gost/x/registry"
|
||||
)
|
||||
|
||||
@@ -24,6 +25,9 @@ func ParseAdmission(cfg *config.AdmissionConfig) admission.Admission {
|
||||
}
|
||||
|
||||
if cfg.Plugin != nil {
|
||||
if err := xplugin.Spawn(cfg.Plugin.Command); err != nil {
|
||||
logger.Default().Errorf("spawn plugin %s: %v", cfg.Name, err)
|
||||
}
|
||||
var tlsCfg *tls.Config
|
||||
if cfg.Plugin.TLS != nil {
|
||||
tlsCfg = &tls.Config{
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/go-gost/x/config"
|
||||
"github.com/go-gost/x/internal/loader"
|
||||
"github.com/go-gost/x/internal/plugin"
|
||||
xplugin "github.com/go-gost/x/plugin"
|
||||
"github.com/go-gost/x/registry"
|
||||
)
|
||||
|
||||
@@ -27,6 +28,9 @@ func ParseAuther(cfg *config.AutherConfig) auth.Authenticator {
|
||||
}
|
||||
|
||||
if cfg.Plugin != nil {
|
||||
if err := xplugin.Spawn(cfg.Plugin.Command); err != nil {
|
||||
logger.Default().Errorf("spawn plugin %s: %v", cfg.Name, err)
|
||||
}
|
||||
var tlsCfg *tls.Config
|
||||
if cfg.Plugin.TLS != nil {
|
||||
tlsCfg = &tls.Config{
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/go-gost/x/config"
|
||||
"github.com/go-gost/x/internal/loader"
|
||||
"github.com/go-gost/x/internal/plugin"
|
||||
xplugin "github.com/go-gost/x/plugin"
|
||||
"github.com/go-gost/x/registry"
|
||||
)
|
||||
|
||||
@@ -23,6 +24,9 @@ func ParseBypass(cfg *config.BypassConfig) bypass.Bypass {
|
||||
}
|
||||
|
||||
if cfg.Plugin != nil {
|
||||
if err := xplugin.Spawn(cfg.Plugin.Command); err != nil {
|
||||
logger.Default().Errorf("spawn plugin %s: %v", cfg.Name, err)
|
||||
}
|
||||
var tlsCfg *tls.Config
|
||||
if cfg.Plugin.TLS != nil {
|
||||
tlsCfg = &tls.Config{
|
||||
|
||||
@@ -15,11 +15,12 @@ import (
|
||||
selector_parser "github.com/go-gost/x/config/parsing/selector"
|
||||
xhop "github.com/go-gost/x/hop"
|
||||
hop_plugin "github.com/go-gost/x/hop/plugin"
|
||||
xs "github.com/go-gost/x/selector"
|
||||
"github.com/go-gost/x/internal/loader"
|
||||
"github.com/go-gost/x/internal/plugin"
|
||||
"github.com/go-gost/x/metadata"
|
||||
mdutil "github.com/go-gost/x/metadata/util"
|
||||
xplugin "github.com/go-gost/x/plugin"
|
||||
xs "github.com/go-gost/x/selector"
|
||||
)
|
||||
|
||||
// ParseHop converts a HopConfig into a hop.Hop. It resolves plugin backends
|
||||
@@ -32,6 +33,9 @@ func ParseHop(cfg *config.HopConfig, log logger.Logger) (hop.Hop, error) {
|
||||
}
|
||||
|
||||
if cfg.Plugin != nil {
|
||||
if err := xplugin.Spawn(cfg.Plugin.Command); err != nil {
|
||||
logger.Default().Errorf("spawn plugin %s: %v", cfg.Name, err)
|
||||
}
|
||||
var tlsCfg *tls.Config
|
||||
if cfg.Plugin.TLS != nil {
|
||||
tlsCfg = &tls.Config{
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
hosts_plugin "github.com/go-gost/x/hosts/plugin"
|
||||
"github.com/go-gost/x/internal/loader"
|
||||
"github.com/go-gost/x/internal/plugin"
|
||||
xplugin "github.com/go-gost/x/plugin"
|
||||
)
|
||||
|
||||
// ParseHostMapper converts a HostsConfig into a hosts.HostMapper. It resolves
|
||||
@@ -23,6 +24,9 @@ func ParseHostMapper(cfg *config.HostsConfig) hosts.HostMapper {
|
||||
}
|
||||
|
||||
if cfg.Plugin != nil {
|
||||
if err := xplugin.Spawn(cfg.Plugin.Command); err != nil {
|
||||
logger.Default().Errorf("spawn plugin %s: %v", cfg.Name, err)
|
||||
}
|
||||
var tlsCfg *tls.Config
|
||||
if cfg.Plugin.TLS != nil {
|
||||
tlsCfg = &tls.Config{
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
ingress_plugin "github.com/go-gost/x/ingress/plugin"
|
||||
"github.com/go-gost/x/internal/loader"
|
||||
"github.com/go-gost/x/internal/plugin"
|
||||
xplugin "github.com/go-gost/x/plugin"
|
||||
)
|
||||
|
||||
// ParseIngress converts an IngressConfig into an ingress.Ingress. It resolves
|
||||
@@ -22,6 +23,9 @@ func ParseIngress(cfg *config.IngressConfig) ingress.Ingress {
|
||||
}
|
||||
|
||||
if cfg.Plugin != nil {
|
||||
if err := xplugin.Spawn(cfg.Plugin.Command); err != nil {
|
||||
logger.Default().Errorf("spawn plugin %s: %v", cfg.Name, err)
|
||||
}
|
||||
var tlsCfg *tls.Config
|
||||
if cfg.Plugin.TLS != nil {
|
||||
tlsCfg = &tls.Config{
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
xrate "github.com/go-gost/x/limiter/rate"
|
||||
xtraffic "github.com/go-gost/x/limiter/traffic"
|
||||
traffic_plugin "github.com/go-gost/x/limiter/traffic/plugin"
|
||||
xplugin "github.com/go-gost/x/plugin"
|
||||
)
|
||||
|
||||
// ParseTrafficLimiter converts a LimiterConfig into a traffic.TrafficLimiter.
|
||||
@@ -26,6 +27,9 @@ func ParseTrafficLimiter(cfg *config.LimiterConfig) (lim traffic.TrafficLimiter)
|
||||
}
|
||||
|
||||
if cfg.Plugin != nil {
|
||||
if err := xplugin.Spawn(cfg.Plugin.Command); err != nil {
|
||||
logger.Default().Errorf("spawn plugin %s: %v", cfg.Name, err)
|
||||
}
|
||||
var tlsCfg *tls.Config
|
||||
if cfg.Plugin.TLS != nil {
|
||||
tlsCfg = &tls.Config{
|
||||
|
||||
@@ -4,10 +4,12 @@ import (
|
||||
"crypto/tls"
|
||||
"strings"
|
||||
|
||||
"github.com/go-gost/core/logger"
|
||||
"github.com/go-gost/core/observer"
|
||||
"github.com/go-gost/x/config"
|
||||
"github.com/go-gost/x/internal/plugin"
|
||||
observer_plugin "github.com/go-gost/x/observer/plugin"
|
||||
xplugin "github.com/go-gost/x/plugin"
|
||||
)
|
||||
|
||||
// ParseObserver converts an ObserverConfig into an observer.Observer. It only
|
||||
@@ -18,6 +20,9 @@ func ParseObserver(cfg *config.ObserverConfig) observer.Observer {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := xplugin.Spawn(cfg.Plugin.Command); err != nil {
|
||||
logger.Default().Errorf("spawn plugin %s: %v", cfg.Name, err)
|
||||
}
|
||||
var tlsCfg *tls.Config
|
||||
if cfg.Plugin.TLS != nil {
|
||||
tlsCfg = &tls.Config{
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/go-gost/x/internal/plugin"
|
||||
xp2p "github.com/go-gost/x/p2p"
|
||||
p2p_plugin "github.com/go-gost/x/p2p/plugin"
|
||||
xplugin "github.com/go-gost/x/plugin"
|
||||
)
|
||||
|
||||
// ParseP2P converts a P2PConfig into a p2p.TunnelProvider backed by an
|
||||
@@ -21,6 +22,9 @@ func ParseP2P(cfg *config.P2PConfig) (p xp2p.TunnelProvider) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := xplugin.Spawn(cfg.Plugin.Command); err != nil {
|
||||
logger.Default().Errorf("spawn plugin %s: %v", cfg.Name, err)
|
||||
}
|
||||
var tlsCfg *tls.Config
|
||||
if cfg.Plugin.TLS != nil {
|
||||
tlsCfg = &tls.Config{
|
||||
@@ -40,4 +44,4 @@ func ParseP2P(cfg *config.P2PConfig) (p xp2p.TunnelProvider) {
|
||||
plugin.TLSConfigOption(tlsCfg),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/go-gost/core/recorder"
|
||||
"github.com/go-gost/x/config"
|
||||
"github.com/go-gost/x/internal/plugin"
|
||||
xplugin "github.com/go-gost/x/plugin"
|
||||
xrecorder "github.com/go-gost/x/recorder"
|
||||
recorder_plugin "github.com/go-gost/x/recorder/plugin"
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
@@ -33,6 +34,9 @@ func ParseRecorder(cfg *config.RecorderConfig) (r recorder.Recorder) {
|
||||
}
|
||||
|
||||
if cfg.Plugin != nil {
|
||||
if err := xplugin.Spawn(cfg.Plugin.Command); err != nil {
|
||||
logger.Default().Errorf("spawn plugin %s: %v", cfg.Name, err)
|
||||
}
|
||||
var tlsCfg *tls.Config
|
||||
if cfg.Plugin.TLS != nil {
|
||||
tlsCfg = &tls.Config{
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/go-gost/core/resolver"
|
||||
"github.com/go-gost/x/config"
|
||||
"github.com/go-gost/x/internal/plugin"
|
||||
xplugin "github.com/go-gost/x/plugin"
|
||||
"github.com/go-gost/x/registry"
|
||||
xresolver "github.com/go-gost/x/resolver"
|
||||
resolver_plugin "github.com/go-gost/x/resolver/plugin"
|
||||
@@ -24,6 +25,9 @@ func ParseResolver(cfg *config.ResolverConfig) (resolver.Resolver, error) {
|
||||
}
|
||||
|
||||
if cfg.Plugin != nil {
|
||||
if err := xplugin.Spawn(cfg.Plugin.Command); err != nil {
|
||||
logger.Default().Errorf("spawn plugin %s: %v", cfg.Name, err)
|
||||
}
|
||||
var tlsCfg *tls.Config
|
||||
if cfg.Plugin.TLS != nil {
|
||||
tlsCfg = &tls.Config{
|
||||
|
||||
@@ -4,9 +4,11 @@ import (
|
||||
"crypto/tls"
|
||||
"strings"
|
||||
|
||||
"github.com/go-gost/core/logger"
|
||||
"github.com/go-gost/core/rewriter"
|
||||
"github.com/go-gost/x/config"
|
||||
"github.com/go-gost/x/internal/plugin"
|
||||
xplugin "github.com/go-gost/x/plugin"
|
||||
rewriter_plugin "github.com/go-gost/x/rewriter/plugin"
|
||||
)
|
||||
|
||||
@@ -19,6 +21,9 @@ func ParseRewriter(cfg *config.RewriterConfig) rewriter.Rewriter {
|
||||
}
|
||||
|
||||
if cfg.Plugin != nil {
|
||||
if err := xplugin.Spawn(cfg.Plugin.Command); err != nil {
|
||||
logger.Default().Errorf("spawn plugin %s: %v", cfg.Name, err)
|
||||
}
|
||||
var tlsCfg *tls.Config
|
||||
if cfg.Plugin.TLS != nil {
|
||||
tlsCfg = &tls.Config{
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/go-gost/x/config"
|
||||
"github.com/go-gost/x/internal/loader"
|
||||
"github.com/go-gost/x/internal/plugin"
|
||||
xplugin "github.com/go-gost/x/plugin"
|
||||
xrouter "github.com/go-gost/x/router"
|
||||
router_plugin "github.com/go-gost/x/router/plugin"
|
||||
)
|
||||
@@ -23,6 +24,9 @@ func ParseRouter(cfg *config.RouterConfig) router.Router {
|
||||
}
|
||||
|
||||
if cfg.Plugin != nil {
|
||||
if err := xplugin.Spawn(cfg.Plugin.Command); err != nil {
|
||||
logger.Default().Errorf("spawn plugin %s: %v", cfg.Name, err)
|
||||
}
|
||||
var tlsCfg *tls.Config
|
||||
if cfg.Plugin.TLS != nil {
|
||||
tlsCfg = &tls.Config{
|
||||
|
||||
@@ -4,9 +4,11 @@ import (
|
||||
"crypto/tls"
|
||||
"strings"
|
||||
|
||||
"github.com/go-gost/core/logger"
|
||||
"github.com/go-gost/core/sd"
|
||||
"github.com/go-gost/x/config"
|
||||
"github.com/go-gost/x/internal/plugin"
|
||||
xplugin "github.com/go-gost/x/plugin"
|
||||
sd_plugin "github.com/go-gost/x/sd/plugin"
|
||||
)
|
||||
|
||||
@@ -17,6 +19,9 @@ func ParseSD(cfg *config.SDConfig) sd.SD {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := xplugin.Spawn(cfg.Plugin.Command); err != nil {
|
||||
logger.Default().Errorf("spawn plugin %s: %v", cfg.Name, err)
|
||||
}
|
||||
var tlsCfg *tls.Config
|
||||
if cfg.Plugin.TLS != nil {
|
||||
tlsCfg = &tls.Config{
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
// Package plugin manages the lifecycle of external plugin subprocesses
|
||||
// spawned from plugin configuration.
|
||||
//
|
||||
// Plugins are normally external processes that GOST connects to over HTTP or
|
||||
// gRPC. When a plugin config carries a non-empty Command, Spawn starts it as a
|
||||
// child process of GOST so that a single invocation brings up the whole stack,
|
||||
// and Shutdown terminates it again.
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
children = map[string]*exec.Cmd{}
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
)
|
||||
|
||||
// Spawn launches command as a child process, deduplicated by its argv string.
|
||||
// An empty command is a no-op. A previously-started command that is still
|
||||
// running is reused. It returns an error only when the process fails to start.
|
||||
func Spawn(command []string) error {
|
||||
if len(command) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
key := strings.Join(command, "\x00")
|
||||
|
||||
mu.Lock()
|
||||
if _, ok := children[key]; ok {
|
||||
// ponytail: no liveness probe; a crashed child is reaped by the Wait
|
||||
// goroutine and respawned on the next reload.
|
||||
mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
if ctx == nil {
|
||||
ctx, cancel = context.WithCancel(context.Background())
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, command[0], command[1:]...)
|
||||
// Stdout/Stderr are left nil so the child's output goes to the null
|
||||
// device; the plugin owns its own log storage.
|
||||
prepare(cmd)
|
||||
if err := cmd.Start(); err != nil {
|
||||
mu.Unlock()
|
||||
return err
|
||||
}
|
||||
attach(cmd)
|
||||
|
||||
children[key] = cmd
|
||||
mu.Unlock()
|
||||
|
||||
go func(key string, cmd *exec.Cmd) {
|
||||
cmd.Wait()
|
||||
mu.Lock()
|
||||
delete(children, key)
|
||||
mu.Unlock()
|
||||
}(key, cmd)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Shutdown terminates all spawned plugin subprocesses.
|
||||
func Shutdown() {
|
||||
if cancel != nil {
|
||||
cancel()
|
||||
}
|
||||
mu.Lock()
|
||||
children = map[string]*exec.Cmd{}
|
||||
mu.Unlock()
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
//go:build linux
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// prepare arranges for the child to receive SIGTERM when the parent process
|
||||
// dies, including an abrupt exit (panic, kill -9). It uses prctl(PR_SET_PDEATHSIG).
|
||||
func prepare(cmd *exec.Cmd) {
|
||||
if cmd.SysProcAttr == nil {
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{}
|
||||
}
|
||||
cmd.SysProcAttr.Pdeathsig = syscall.SIGTERM
|
||||
}
|
||||
|
||||
func attach(cmd *exec.Cmd) {}
|
||||
@@ -0,0 +1,12 @@
|
||||
//go:build !linux && !windows
|
||||
|
||||
package plugin
|
||||
|
||||
import "os/exec"
|
||||
|
||||
// prepare and attach are no-ops on platforms without a parent-side mechanism
|
||||
// to terminate the child on parent death. Children spawned here will not
|
||||
// follow an abrupt parent exit (panic / kill -9).
|
||||
func prepare(cmd *exec.Cmd) {}
|
||||
|
||||
func attach(cmd *exec.Cmd) {}
|
||||
@@ -0,0 +1,77 @@
|
||||
//go:build windows
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"sync"
|
||||
"unsafe"
|
||||
|
||||
"github.com/go-gost/core/logger"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var (
|
||||
jobOnce sync.Once
|
||||
job windows.Handle
|
||||
jobErr error
|
||||
)
|
||||
|
||||
func initJob() {
|
||||
handle, err := windows.CreateJobObject(nil, nil)
|
||||
if err != nil {
|
||||
jobErr = err
|
||||
logger.Default().Errorf("plugin: create job object: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
info := windows.JOBOBJECT_EXTENDED_LIMIT_INFORMATION{
|
||||
BasicLimitInformation: windows.JOBOBJECT_BASIC_LIMIT_INFORMATION{
|
||||
LimitFlags: windows.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
|
||||
},
|
||||
}
|
||||
if _, err := windows.SetInformationJobObject(
|
||||
handle,
|
||||
windows.JobObjectExtendedLimitInformation,
|
||||
uintptr(unsafe.Pointer(&info)),
|
||||
uint32(unsafe.Sizeof(info)),
|
||||
); err != nil {
|
||||
jobErr = err
|
||||
windows.CloseHandle(handle)
|
||||
logger.Default().Errorf("plugin: configure job object: %v", err)
|
||||
return
|
||||
}
|
||||
job = handle
|
||||
}
|
||||
|
||||
// processAllAccess is PROCESS_ALL_ACCESS, needed to open the child for
|
||||
// AssignProcessToJobObject.
|
||||
const processAllAccess = windows.STANDARD_RIGHTS_REQUIRED | windows.SYNCHRONIZE | 0xffff
|
||||
|
||||
func prepare(cmd *exec.Cmd) {}
|
||||
|
||||
// attach assigns the started child to the shared job object whose
|
||||
// KILL_ON_JOB_CLOSE flag makes the OS terminate the child when the parent
|
||||
// process dies (even abruptly). Failure (e.g. the parent is already in a job)
|
||||
// is non-fatal: the child simply loses the die-with-parent guarantee.
|
||||
func attach(cmd *exec.Cmd) {
|
||||
if cmd.Process == nil {
|
||||
return
|
||||
}
|
||||
|
||||
jobOnce.Do(initJob)
|
||||
if jobErr != nil {
|
||||
return
|
||||
}
|
||||
|
||||
handle, err := windows.OpenProcess(processAllAccess, false, uint32(cmd.Process.Pid))
|
||||
if err != nil {
|
||||
logger.Default().Errorf("plugin: open child process: %v", err)
|
||||
return
|
||||
}
|
||||
defer windows.CloseHandle(handle)
|
||||
|
||||
if err := windows.AssignProcessToJobObject(job, handle); err != nil {
|
||||
logger.Default().Errorf("plugin: assign child to job object: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user