305 lines
7.9 KiB
Go
305 lines
7.9 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"syscall"
|
|
"time"
|
|
|
|
"vpnclient/internal/config"
|
|
"vpnclient/internal/core"
|
|
"vpnclient/internal/protocols/hysteria2"
|
|
"vpnclient/internal/protocols/naive"
|
|
"vpnclient/internal/protocols/xray"
|
|
"vpnclient/internal/update"
|
|
)
|
|
|
|
func main() {
|
|
if len(os.Args) < 2 {
|
|
printUsage()
|
|
os.Exit(2)
|
|
}
|
|
|
|
cmd := os.Args[1]
|
|
args := os.Args[2:]
|
|
|
|
switch cmd {
|
|
case "connect":
|
|
os.Exit(runConnect(args))
|
|
case "disconnect":
|
|
fmt.Fprintln(os.Stderr, "disconnect: use Ctrl+C on a running connect session")
|
|
os.Exit(2)
|
|
case "status":
|
|
fmt.Fprintln(os.Stderr, "status: only available while connect is running in this process")
|
|
os.Exit(2)
|
|
case "install-core":
|
|
os.Exit(runInstallCore(args))
|
|
case "init":
|
|
os.Exit(runInit(args))
|
|
case "probe":
|
|
os.Exit(runProbe(args))
|
|
case "set-proxy":
|
|
os.Exit(runSetProxy(args))
|
|
case "check-update":
|
|
os.Exit(runCheckUpdate(args))
|
|
case "apply-update":
|
|
os.Exit(runApplyUpdate(args))
|
|
case "help", "-h", "--help":
|
|
printUsage()
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "unknown command: %s\n", cmd)
|
|
printUsage()
|
|
os.Exit(2)
|
|
}
|
|
}
|
|
|
|
func printUsage() {
|
|
fmt.Fprintf(os.Stderr, `Navis VPN client (Naive / Hy2 / AWG / VLESS / VMess / Trojan) — Windows / macOS
|
|
|
|
Usage:
|
|
navis init [-config configs/config.json]
|
|
navis install-core [-config configs/config.json]
|
|
navis set-proxy [-config configs/config.json] <share-or-proxy-uri>
|
|
navis connect [-config configs/config.json] [-profile name] [-no-sysproxy] [-probe]
|
|
navis probe [-config configs/config.json] [-profile name] [-url URL]
|
|
navis check-update
|
|
navis apply-update
|
|
|
|
Share links: naive+https://… hysteria2://… vless://… vmess://… trojan://… awg .conf
|
|
|
|
On macOS system proxy uses networksetup (HTTP + SOCKS). Local listens:
|
|
socks://127.0.0.1:1080 http://127.0.0.1:1081
|
|
|
|
`)
|
|
}
|
|
|
|
func defaultConfigPath() string {
|
|
return filepath.Join("configs", "config.json")
|
|
}
|
|
|
|
func runInit(args []string) int {
|
|
fs := flag.NewFlagSet("init", flag.ExitOnError)
|
|
cfgPath := fs.String("config", defaultConfigPath(), "path to write example config")
|
|
_ = fs.Parse(args)
|
|
if _, err := os.Stat(*cfgPath); err == nil {
|
|
fmt.Fprintf(os.Stderr, "refusing to overwrite existing %s\n", *cfgPath)
|
|
return 1
|
|
}
|
|
if err := config.WriteExample(*cfgPath); err != nil {
|
|
fmt.Fprintf(os.Stderr, "init: %v\n", err)
|
|
return 1
|
|
}
|
|
fmt.Printf("wrote %s — edit proxy user/pass/host, then: vpnclient install-core && vpnclient connect\n", *cfgPath)
|
|
return 0
|
|
}
|
|
|
|
func runInstallCore(args []string) int {
|
|
fs := flag.NewFlagSet("install-core", flag.ExitOnError)
|
|
cfgPath := fs.String("config", defaultConfigPath(), "config path (for bin_dir)")
|
|
binDirFlag := fs.String("bin-dir", "", "override bin directory")
|
|
_ = fs.Parse(args)
|
|
|
|
binDir := *binDirFlag
|
|
if binDir == "" {
|
|
if cfg, err := config.Load(*cfgPath); err == nil {
|
|
resolved, err := config.ResolveBinDir(*cfgPath, cfg.BinDir)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "install-core: %v\n", err)
|
|
return 1
|
|
}
|
|
binDir = resolved
|
|
} else {
|
|
binDir = "bin"
|
|
}
|
|
}
|
|
pathNaive, err := naive.EnsureBinary(binDir)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "install-core naive: %v\n", err)
|
|
return 1
|
|
}
|
|
fmt.Printf("naive core ready: %s\n", pathNaive)
|
|
pathHy2, err := hysteria2.EnsureBinary(binDir)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "install-core hysteria2: %v\n", err)
|
|
return 1
|
|
}
|
|
fmt.Printf("hysteria2 core ready: %s\n", pathHy2)
|
|
pathXray, err := xray.EnsureBinary(binDir)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "install-core xray: %v\n", err)
|
|
return 1
|
|
}
|
|
fmt.Printf("xray core ready: %s\n", pathXray)
|
|
return 0
|
|
}
|
|
|
|
func loadManager(cfgPath string) (*core.Manager, *config.Config, error) {
|
|
cfg, err := config.Load(cfgPath)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
mgr, err := core.NewManager(cfgPath, cfg, os.Stderr)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return mgr, cfg, nil
|
|
}
|
|
|
|
func runConnect(args []string) int {
|
|
fs := flag.NewFlagSet("connect", flag.ExitOnError)
|
|
cfgPath := fs.String("config", defaultConfigPath(), "config path")
|
|
profile := fs.String("profile", "", "profile name (default: config.active)")
|
|
noSys := fs.Bool("no-sysproxy", false, "do not change Windows system proxy")
|
|
doProbe := fs.Bool("probe", false, "verify tunnel with an HTTP request after connect")
|
|
_ = fs.Parse(args)
|
|
|
|
mgr, cfg, err := loadManager(*cfgPath)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "connect: %v\n", err)
|
|
return 1
|
|
}
|
|
if *noSys {
|
|
cfg.SystemProxy = false
|
|
}
|
|
|
|
// Ensure official core is present.
|
|
if _, err := mgr.EnsureCore(""); err != nil {
|
|
fmt.Fprintf(os.Stderr, "connect: %v\n", err)
|
|
return 1
|
|
}
|
|
|
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
|
defer stop()
|
|
|
|
if err := mgr.Connect(ctx, *profile); err != nil {
|
|
fmt.Fprintf(os.Stderr, "connect: %v\n", err)
|
|
return 1
|
|
}
|
|
defer func() {
|
|
if err := mgr.Disconnect(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "disconnect: %v\n", err)
|
|
}
|
|
}()
|
|
|
|
st := mgr.Status()
|
|
enc := json.NewEncoder(os.Stdout)
|
|
enc.SetIndent("", " ")
|
|
_ = enc.Encode(st)
|
|
fmt.Fprintln(os.Stderr, "connected — press Ctrl+C to disconnect and restore system proxy")
|
|
|
|
if *doProbe {
|
|
pctx, cancel := context.WithTimeout(ctx, 45*time.Second)
|
|
err := mgr.Probe(pctx, "")
|
|
cancel()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "probe failed: %v\n", err)
|
|
} else {
|
|
fmt.Fprintln(os.Stderr, "probe ok")
|
|
}
|
|
}
|
|
|
|
<-ctx.Done()
|
|
fmt.Fprintln(os.Stderr, "shutting down...")
|
|
return 0
|
|
}
|
|
|
|
func runProbe(args []string) int {
|
|
fs := flag.NewFlagSet("probe", flag.ExitOnError)
|
|
cfgPath := fs.String("config", defaultConfigPath(), "config path")
|
|
profile := fs.String("profile", "", "profile name")
|
|
testURL := fs.String("url", "https://www.google.com/generate_204", "URL to fetch via tunnel")
|
|
_ = fs.Parse(args)
|
|
|
|
mgr, cfg, err := loadManager(*cfgPath)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "probe: %v\n", err)
|
|
return 1
|
|
}
|
|
cfg.SystemProxy = false
|
|
|
|
if _, err := mgr.EnsureCore(""); err != nil {
|
|
fmt.Fprintf(os.Stderr, "probe: %v\n", err)
|
|
return 1
|
|
}
|
|
|
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
|
defer stop()
|
|
|
|
if err := mgr.Connect(ctx, *profile); err != nil {
|
|
fmt.Fprintf(os.Stderr, "probe: connect: %v\n", err)
|
|
return 1
|
|
}
|
|
defer mgr.Disconnect()
|
|
|
|
pctx, cancel := context.WithTimeout(ctx, 45*time.Second)
|
|
defer cancel()
|
|
if err := mgr.Probe(pctx, *testURL); err != nil {
|
|
fmt.Fprintf(os.Stderr, "probe failed: %v\n", err)
|
|
return 1
|
|
}
|
|
fmt.Println("probe ok")
|
|
return 0
|
|
}
|
|
|
|
func runSetProxy(args []string) int {
|
|
fs := flag.NewFlagSet("set-proxy", flag.ExitOnError)
|
|
cfgPath := fs.String("config", defaultConfigPath(), "config path")
|
|
_ = fs.Parse(args)
|
|
rest := fs.Args()
|
|
if len(rest) < 1 {
|
|
fmt.Fprintln(os.Stderr, "usage: vpnclient set-proxy <uri>")
|
|
return 2
|
|
}
|
|
mgr, _, err := loadManager(*cfgPath)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "set-proxy: %v\n", err)
|
|
return 1
|
|
}
|
|
if err := mgr.UpdateProxyURI(rest[0]); err != nil {
|
|
fmt.Fprintf(os.Stderr, "set-proxy: %v\n", err)
|
|
return 1
|
|
}
|
|
fmt.Println("proxy saved")
|
|
return 0
|
|
}
|
|
|
|
func runCheckUpdate(args []string) int {
|
|
_ = args
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
st, err := update.Check(ctx, update.DefaultManifestURL)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "check-update: %v\n", err)
|
|
return 1
|
|
}
|
|
fmt.Printf("current=%s platform=%s latest=%s available=%v\n", st.Current, st.Platform, st.Latest, st.Available)
|
|
if st.Notes != "" {
|
|
fmt.Println(st.Notes)
|
|
}
|
|
if st.Error != "" && !st.Available {
|
|
fmt.Fprintf(os.Stderr, "note: %s\n", st.Error)
|
|
}
|
|
if st.Available {
|
|
fmt.Printf("url=%s\n", st.URL)
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func runApplyUpdate(args []string) int {
|
|
_ = args
|
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
|
|
defer cancel()
|
|
msg, err := update.Apply(ctx, update.DefaultManifestURL)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "apply-update: %v\n", err)
|
|
return 1
|
|
}
|
|
fmt.Println(msg)
|
|
return 0
|
|
}
|