// proxyctl — host-side CLI for the on-device HTTPS MITM (appproxy). // // Build the android binary, push it, install/reinject the CA, start/stop the // proxy, run discover mode, and pull captures into outputs/discover/. // Package proxyctlcmd is the on-device MITM host CLI, exposed as a library so the // single drm binary can host it as a subcommand. package proxyctlcmd import ( "flag" "fmt" "os" "os/exec" "os/signal" "path/filepath" "runtime" "strings" "syscall" "time" "drmdecryption/adb" "drmdecryption/proxy" "drmdecryption/repo" ) // Run dispatches a proxy subcommand. args[0] is the subcommand name. func Run(args []string) error { if len(args) < 1 { Usage() return fmt.Errorf("proxy: subcommand required") } sub, rest := args[0], args[1:] switch sub { case "build": buildCmd(rest) case "push": pushCmd(rest) case "install-ca": installCACmd(rest) case "reinject-ca": reinjectCACmd(rest) case "start": startCmd(rest) case "stop": stopCmd(rest) case "discover": discoverCmd(rest) case "pull": pullCmd(rest) case "clear-proxy": clearProxyCmd(rest) case "transparent", "tproxy": transparentCmd(rest) case "help", "-h", "--help": Usage() default: Usage() return fmt.Errorf("proxy: unknown subcommand %q", sub) } return nil } // Usage prints the proxy subcommand help. func Usage() { fmt.Fprintf(os.Stderr, `proxyctl — on-device MITM (appproxy) host control Usage: proxyctl build cross-compile linux/arm64 → bin/ + apps/proxy/ proxyctl push [--serial S] push binary to /data/local/tmp/appproxy proxyctl install-ca [--serial S] [--ca PATH] [--reinject] push CA + HASH.0; optional Magisk reinject proxyctl reinject-ca [--serial S] [--hash HASH] Magisk conscrypt bind only (CA already on device) proxyctl start [--serial S] [--bin PATH] [--install-ca] [--reinject] stop old → push → start → set http_proxy proxyctl stop [--serial S] kill the on-device proxy + clear http_proxy proxyctl discover [--serial S] [--out DIR] [--skip-build] [--install-ca] [--reinject] -log-all session; Ctrl+C → outputs/discover/ proxyctl pull [--serial S] [--out DIR] pull traffic/cap/log into outputs/discover/ proxyctl clear-proxy [--serial S] clear global http_proxy (+ stop mitm) proxyctl transparent --package PKG [--serial S] [--out DIR] [--reinject] iptables REDIRECT capture (UK VPN OK; no Wi‑Fi proxy) writes /data/local/tmp/capture// ; adb-pulled to --out Artifacts land under outputs/discover// by default. Transparent captures pull into outputs/transparent// by default. `) } func clientFrom(fs *flag.FlagSet, args []string) *adb.Client { serial := fs.String("serial", "", "adb device serial") _ = fs.Parse(args) c := adb.New() if *serial != "" { c = c.WithSerial(*serial) } return c } func buildCmd(args []string) { fs := flag.NewFlagSet("build", flag.ExitOnError) _ = fs.Parse(args) root := repo.Root() deviceDir := filepath.Join(root, "apps", "proxy", "device") outLocal := filepath.Join(root, "apps", "proxy", "proxy-android-arm64") outBin := filepath.Join(root, "bin", "proxy-android-arm64") fmt.Println("[*] Building linux/arm64 appproxy...") cmd := exec.Command("go", "build", "-ldflags=-s -w", "-o", outLocal, ".") cmd.Dir = deviceDir cmd.Env = append(os.Environ(), "GOOS=linux", "GOARCH=arm64", "CGO_ENABLED=0", ) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { fmt.Fprintf(os.Stderr, "build failed: %v\n", err) os.Exit(1) } _ = os.MkdirAll(filepath.Join(root, "bin"), 0o755) data, err := os.ReadFile(outLocal) if err != nil { fmt.Fprintf(os.Stderr, "read binary: %v\n", err) os.Exit(1) } if err := os.WriteFile(outBin, data, 0o755); err != nil { fmt.Fprintf(os.Stderr, "copy to bin/: %v\n", err) os.Exit(1) } // Compat name next to the canonical one. // Legacy copy so hosts that still look for the old name keep working. _ = os.WriteFile(filepath.Join(root, "apps", "proxy", "rteproxy-android-arm64"), data, 0o755) fmt.Println("[+] apps/proxy/proxy-android-arm64") fmt.Println("[+] bin/proxy-android-arm64") } func pushCmd(args []string) { fs := flag.NewFlagSet("push", flag.ExitOnError) bin := fs.String("bin", "", "local proxy binary (default: auto)") c := clientFrom(fs, args) if err := c.EnsureDevice(); err != nil { fatal(err) } local := *bin if local == "" { local = proxy.FindLocalBin("") } if local == "" { fatal(fmt.Errorf("no proxy binary — run: proxyctl build")) } fmt.Printf("[*] Pushing %s → %s\n", local, proxy.RemoteBin) if err := c.Push(local, proxy.RemoteBin); err != nil { fatal(err) } _, _ = c.Shell("chmod", "755", proxy.RemoteBin) fmt.Println("[+] pushed") } func installCACmd(args []string) { fs := flag.NewFlagSet("install-ca", flag.ExitOnError) ca := fs.String("ca", "", "local CA PEM (default: the CA pulled from the device)") reinject := fs.Bool("reinject", false, "Magisk-reinject into conscrypt after push") c := clientFrom(fs, args) if err := c.EnsureDevice(); err != nil { fatal(err) } hash, err := proxy.InstallCA(c, *ca, *reinject) if err != nil { fatal(err) } fmt.Printf("[+] CA hash %s installed on device\n", hash) } func reinjectCACmd(args []string) { fs := flag.NewFlagSet("reinject-ca", flag.ExitOnError) hash := fs.String("hash", proxy.DefaultCAHash, "Android CA subject_hash_old") c := clientFrom(fs, args) if err := c.EnsureDevice(); err != nil { fatal(err) } proxy.ReinjectCA(c, *hash) } func startCmd(args []string) { fs := flag.NewFlagSet("start", flag.ExitOnError) bin := fs.String("bin", "", "local proxy binary (default: auto)") installCA := fs.Bool("install-ca", false, "push CA + HASH.0 before start") reinject := fs.Bool("reinject", false, "Magisk-reinject CA (implies -install-ca)") c := clientFrom(fs, args) if err := c.EnsureDevice(); err != nil { fatal(err) } if *reinject { *installCA = true } if *installCA { if _, err := proxy.InstallCA(c, "", *reinject); err != nil { fatal(err) } } local := *bin if local == "" { local = proxy.FindLocalBin("") } if local == "" { fatal(fmt.Errorf("no proxy binary — run: proxyctl build")) } if err := proxy.PushAndStart(c, local); err != nil { fatal(err) } if err := proxy.EnsureHTTPProxy(c, ""); err != nil { fatal(err) } } func stopCmd(args []string) { fs := flag.NewFlagSet("stop", flag.ExitOnError) c := clientFrom(fs, args) proxy.Stop(c) proxy.ClearHTTPProxy(c) } func clearProxyCmd(args []string) { fs := flag.NewFlagSet("clear-proxy", flag.ExitOnError) c := clientFrom(fs, args) proxy.ClearHTTPProxy(c) } func transparentCmd(args []string) { fs := flag.NewFlagSet("transparent", flag.ExitOnError) pkg := fs.String("package", "", "app package to redirect (e.g. bbc.iplayer.android)") out := fs.String("out", "", "host pull dir (default: outputs/transparent/)") bin := fs.String("bin", "", "local proxy binary (default: auto)") reinject := fs.Bool("reinject", false, "Magisk-reinject CA before start (slow; CA usually already mounted)") port := fs.String("port", "8080", "transparent listen port on device") noTail := fs.Bool("no-tail", false, "start only; do not wait / pull on Ctrl+C") c := clientFrom(fs, args) if err := c.EnsureDevice(); err != nil { fatal(err) } if strings.TrimSpace(*pkg) == "" { fatal(fmt.Errorf("--package is required (e.g. --package bbc.iplayer.android)")) } local := *bin if local == "" { local = proxy.FindLocalBin("") } if local == "" { fatal(fmt.Errorf("no proxy binary — run: proxyctl build")) } // Never leave a stale Wi‑Fi proxy when using transparent mode. proxy.ClearHTTPProxy(c) remoteDir := "/data/local/tmp/capture/" + *pkg fmt.Printf("[*] Transparent capture for %s → %s\n", *pkg, remoteDir) if err := proxy.StartTransparent(c, local, *pkg, *port, remoteDir, *reinject); err != nil { fatal(err) } fmt.Println("[+] running. Leave UK VPN ON. Open the app and play.") fmt.Println(" Ctrl+C → stop iptables + pull captures") if *noTail { return } dest := *out if dest == "" { dest = filepath.Join(repo.Root(), "outputs", "transparent", time.Now().Format("20060102-150405")) } sig := make(chan os.Signal, 1) signal.Notify(sig, os.Interrupt) <-sig fmt.Println("\n[*] Stopping transparent capture...") proxy.StopTransparent(c) _ = os.MkdirAll(dest, 0o755) if err := proxy.PullDir(c, remoteDir, dest); err != nil { fmt.Fprintf(os.Stderr, "pull: %v\n", err) } else { fmt.Println("[+] pulled into", dest) } } func pullCmd(args []string) { fs := flag.NewFlagSet("pull", flag.ExitOnError) out := fs.String("out", "", "destination dir (default: outputs/discover/)") c := clientFrom(fs, args) if err := c.EnsureDevice(); err != nil { fatal(err) } dest := *out if dest == "" { dest = proxy.DiscoverOutDir("", "") } if err := proxy.PullCaptures(c, dest); err != nil { fatal(err) } fmt.Println("[+] pulled into", dest) } func discoverCmd(args []string) { fs := flag.NewFlagSet("discover", flag.ExitOnError) out := fs.String("out", "", "destination dir (default: outputs/discover/)") skipBuild := fs.Bool("skip-build", false, "do not rebuild the android binary") installCA := fs.Bool("install-ca", true, "push CA + HASH.0 before discover") reinject := fs.Bool("reinject", true, "Magisk-reinject CA (default on for discover)") noTail := fs.Bool("no-tail", false, "start only; do not tail / wait for Ctrl+C") listen := fs.String("listen", ":8080", "proxy listen address on device") pkgForce := fs.String("force-stop", "", "package to force-stop after CA reinject (so it inherits the new mount)") c := clientFrom(fs, args) root := repo.Root() dest := *out if dest == "" { dest = proxy.DiscoverOutDir(root, "") } _ = os.MkdirAll(dest, 0o755) if !*skipBuild { buildCmd(nil) } if err := c.EnsureDevice(); err != nil { fatal(err) } fmt.Println("[*] Device:", c.Out("get-serialno")) local := proxy.FindLocalBin(root) if local == "" { fatal(fmt.Errorf("no proxy binary — run: proxyctl build")) } if *installCA || *reinject { if _, err := proxy.InstallCA(c, "", *reinject); err != nil { fmt.Fprintf(os.Stderr, "[!] install-ca: %v\n", err) } } if *pkgForce != "" { c.ForceStop(*pkgForce) fmt.Printf("[*] Force-stopped %s\n", *pkgForce) } proxy.Stop(c) fmt.Printf("[*] Pushing %s → %s (discover / -log-all)\n", local, proxy.RemoteBin) if err := c.Push(local, proxy.RemoteBin); err != nil { fatal(err) } _, _ = c.Shell("chmod", "755", proxy.RemoteBin) _, _ = c.Shell("rm", "-f", proxy.RemoteLog, proxy.RemoteCap, proxy.RemoteTraffic) starter := "#!/system/bin/sh\n" + "exec " + proxy.RemoteBin + " -listen " + *listen + " -out " + proxy.RemoteCap + " -ca-dir /data/local/tmp" + " -dns 1.1.1.1,1.0.0.1,8.8.8.8,192.168.1.1" + " -log-all -traffic " + proxy.RemoteTraffic + " -v >>" + proxy.RemoteLog + " 2>&1\n" tmp := filepath.Join(os.TempDir(), "start_appproxy_discover.sh") if err := os.WriteFile(tmp, []byte(starter), 0o755); err != nil { fatal(err) } defer os.Remove(tmp) if err := c.Push(tmp, "/data/local/tmp/start_appproxy.sh"); err != nil { fatal(err) } _, _ = c.Shell("chmod", "755", "/data/local/tmp/start_appproxy.sh") _, _, _ = c.Run("shell", "setsid /data/local/tmp/start_appproxy.sh /dev/null 2>&1 &") ok := false var pid, head string for i := 0; i < 12; i++ { time.Sleep(400 * time.Millisecond) pid = c.Out("shell", "pidof", "appproxy") if pid == "" { // Legacy process name. pid = c.Out("shell", "pidof", "rteproxy") } head = c.Out("shell", "head", "-20", proxy.RemoteLog) if pid != "" && containsListening(head) { ok = true break } } if head != "" { fmt.Println(head) } if !ok { fmt.Fprintln(os.Stderr, c.Out("shell", "cat", proxy.RemoteLog)) fatal(fmt.Errorf("appproxy failed to start")) } fmt.Printf("[+] appproxy pid=%s\n", pid) if err := proxy.EnsureHTTPProxy(c, ""); err != nil { fatal(err) } fmt.Println() fmt.Println("=== Discover mode ready ===") fmt.Println("1. Unlock the phone and open the target app.") fmt.Println("2. Start playback so DRM + manifest traffic flows.") fmt.Println("3. Ctrl+C stops the tail and pulls captures.") fmt.Println() fmt.Println("Local folder:", dest) fmt.Println() if *noTail { fmt.Println("Started without tail (-no-tail). Pull later with: proxyctl pull") return } sig := make(chan os.Signal, 1) signal.Notify(sig, os.Interrupt, syscall.SIGTERM) tailDone := make(chan struct{}) go func() { defer close(tailDone) cmd := exec.Command(c.Bin, append(serialArgs(c), "shell", "tail", "-f", proxy.RemoteLog)...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr _ = cmd.Run() }() select { case <-sig: fmt.Println() fmt.Println("[*] Stopping tail...") case <-tailDone: } fmt.Println("[*] Pulling captures into", dest) _ = proxy.PullCaptures(c, dest) proxy.ClearHTTPProxy(c) fmt.Println("[+] Done. Inspect:") entries, _ := os.ReadDir(dest) for _, e := range entries { info, _ := e.Info() size := int64(0) if info != nil { size = info.Size() } fmt.Printf(" %s (%d bytes)\n", e.Name(), size) } tip := filepath.Join(dest, "appproxy_traffic.jsonl") if runtime.GOOS == "windows" { fmt.Printf("Tip: Select-String -Path '%s' -Pattern 'mpd|license|widevine|manifest'\n", tip) } else { fmt.Printf("Tip: grep -E 'mpd|license|widevine|manifest' %s\n", tip) } } func serialArgs(c *adb.Client) []string { if c.Serial == "" { return nil } return []string{"-s", c.Serial} } func containsListening(s string) bool { return strings.Contains(strings.ToLower(s), "listening") } func fatal(err error) { fmt.Fprintf(os.Stderr, "proxyctl: %v\n", err) os.Exit(1) }