Files
navi/internal/protocols/naive/engine.go
T

222 lines
4.6 KiB
Go

package naive
import (
"context"
"encoding/json"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"sync"
"time"
"vpnclient/internal/config"
)
// Engine runs the official Chromium-based naive binary.
// NaiveProxy cannot be reimplemented in pure Go: TLS/H2 fingerprints
// must match Chrome, which requires the upstream naive.exe.
type Engine struct {
mu sync.Mutex
cmd *exec.Cmd
cancel context.CancelFunc
done chan struct{}
workdir string
profile config.Profile
stderr io.Writer
running bool
}
func New(stderr io.Writer) *Engine {
if stderr == nil {
stderr = os.Stderr
}
return &Engine{stderr: stderr}
}
func (e *Engine) Protocol() config.Protocol { return config.ProtocolNaive }
func (e *Engine) Start(ctx context.Context, profile config.Profile, binDir string) error {
e.mu.Lock()
defer e.mu.Unlock()
if e.running {
return fmt.Errorf("naive: already running")
}
bin, err := ResolveBinary(binDir)
if err != nil {
return err
}
workdir, err := os.MkdirTemp("", "vpnclient-naive-*")
if err != nil {
return fmt.Errorf("naive: temp dir: %w", err)
}
cfgPath := filepath.Join(workdir, "config.json")
if err := writeRuntimeConfig(cfgPath, profile); err != nil {
os.RemoveAll(workdir)
return err
}
runCtx, cancel := context.WithCancel(context.Background())
cmd := exec.CommandContext(runCtx, bin, cfgPath)
cmd.Dir = workdir
cmd.Stdout = e.stderr
cmd.Stderr = e.stderr
applySysProcAttr(cmd)
env := os.Environ()
for k, v := range profile.Env {
env = append(env, k+"="+v)
}
cmd.Env = env
if err := cmd.Start(); err != nil {
cancel()
os.RemoveAll(workdir)
return fmt.Errorf("naive: start %s: %w", bin, err)
}
done := make(chan struct{})
e.cmd = cmd
e.cancel = cancel
e.done = done
e.workdir = workdir
e.profile = profile
e.running = true
go func() {
err := cmd.Wait()
if err != nil {
fmt.Fprintf(e.stderr, "naive: process ended: %v\n", err)
}
e.mu.Lock()
e.cleanupLocked()
e.mu.Unlock()
close(done)
}()
// Release lock while waiting so the Wait goroutine can clean up.
e.mu.Unlock()
timer := time.NewTimer(800 * time.Millisecond)
defer timer.Stop()
var startErr error
select {
case <-done:
startErr = fmt.Errorf("naive: process exited immediately; check proxy URI, credentials, and binary")
case <-ctx.Done():
_ = e.Stop()
startErr = ctx.Err()
case <-timer.C:
e.mu.Lock()
alive := e.running
e.mu.Unlock()
if !alive {
startErr = fmt.Errorf("naive: process exited during startup")
}
}
e.mu.Lock()
return startErr
}
func (e *Engine) Stop() error {
e.mu.Lock()
defer e.mu.Unlock()
return e.stopLocked()
}
func (e *Engine) stopLocked() error {
if !e.running || e.cmd == nil {
return nil
}
done := e.done
if e.cancel != nil {
e.cancel()
}
proc := e.cmd.Process
e.mu.Unlock()
select {
case <-done:
case <-time.After(2 * time.Second):
if proc != nil {
_ = proc.Kill()
}
<-done
}
e.mu.Lock()
return nil
}
func (e *Engine) cleanupLocked() {
if e.workdir != "" {
_ = os.RemoveAll(e.workdir)
e.workdir = ""
}
e.cmd = nil
e.cancel = nil
e.running = false
}
func (e *Engine) Running() bool {
e.mu.Lock()
defer e.mu.Unlock()
return e.running
}
func (e *Engine) LocalHTTPProxy() (string, bool) {
e.mu.Lock()
defer e.mu.Unlock()
if !e.running {
return "", false
}
return e.profile.HTTPListenHostPort()
}
func (e *Engine) LocalSOCKSProxy() (string, bool) {
e.mu.Lock()
defer e.mu.Unlock()
if !e.running {
return "", false
}
return e.profile.SOCKSListenHostPort()
}
type runtimeConfig struct {
Listen any `json:"listen"`
Proxy string `json:"proxy"`
InsecureConcurrency int `json:"insecure-concurrency,omitempty"`
ExtraHeaders string `json:"extra-headers,omitempty"`
HostResolverRules string `json:"host-resolver-rules,omitempty"`
Log string `json:"log,omitempty"`
NoPostQuantum bool `json:"no-post-quantum,omitempty"`
}
func writeRuntimeConfig(path string, profile config.Profile) error {
listen := profile.DefaultListen()
var listenField any = listen
if len(listen) == 1 {
listenField = listen[0]
}
proxy, err := NormalizeProxyURI(profile.Proxy)
if err != nil {
return err
}
rc := runtimeConfig{
Listen: listenField,
Proxy: proxy,
InsecureConcurrency: profile.InsecureConcurrency,
ExtraHeaders: profile.ExtraHeaders,
HostResolverRules: profile.HostResolverRules,
Log: profile.Log,
NoPostQuantum: profile.NoPostQuantum,
}
data, err := json.MarshalIndent(rc, "", " ")
if err != nil {
return err
}
data = append(data, '\n')
return os.WriteFile(path, data, 0o600)
}