Null-DRM-Official/apps/proxy/proxyctlcmd/proxyctlcmd.go
404errordeveloper 2fa8f2435f Initial commit: Null DRM Official
Capture, decrypt, and restream toolkit with compiled-in app modules
(RTE, TG4, BBC), on-device MITM proxy, streamd control plane, and www.
BBC module.yaml is published (clear streams); other module values stay local.
2026-10-06 00:25:35 +02:00

468 lines
14 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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/<stamp>.
// 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/<stamp>
proxyctl pull [--serial S] [--out DIR]
pull traffic/cap/log into outputs/discover/<stamp>
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/<pkg>/ ; adb-pulled to --out
Artifacts land under outputs/discover/<timestamp>/ by default.
Transparent captures pull into outputs/transparent/<stamp>/ 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/<stamp>)")
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/<stamp>)")
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/<stamp>)")
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 >/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)
}