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] 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 ") 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 }