222 lines
4.6 KiB
Go
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)
|
|
}
|