# go-ios: iOS automation from Linux, and the root it needs

> go-ios is a Go CLI that installs apps, runs XCTests, captures packets and drives an iOS device from Linux, Windows or macOS, and its go.mod is effectively a protocol map: gvisor for the userspace network stack, quic-go for the iOS 17 tunnel, wintun on Windows, go-macho and go-dwarf for reading binaries off the device. Getting there on iOS 17 and later requires a root tunnel daemon, and a second root binary for raw USB.

**danielpaulus/go-ios** — This is an operating system independent implementation of iOS device features. You can run UI tests, launch or kill apps, install apps etc. with it. 

- Repository: https://github.com/danielpaulus/go-ios
- Stars: 2,283 · Forks: 337
- Language: Go
- License: MIT
- Published: 2026-09-30 · Updated: 2026-09-30 · Language: en
- Canonical page: https://hysenlabs.com/projects/danielpaulus-go-ios

## Two root daemons, and the Makefile says so in a comment

The operational requirement that will decide your adoption is in the first comment of the Makefile, and it is unusually honest about it.

The comment says the Makefile builds and runs go-ios and the cdc-ncm network driver, and that cdc-ncm needs to be executed with sudo on Linux for USB Access and setting up virtual TAP network devices. It notes that make build builds both binaries, that make run is a simple target that just runs the cdc-ncm driver with sudo, and that make up is a target to rebuild and run cdc-ncm quickly for development.

So there are two privileged things, not one, and they are separate binaries.

The first is the tunnel. The README states that for iOS 17 and later devices you need to run sudo ios tunnel start for go-ios to work, and that this starts a tunnel daemon. That requirement comes from Apple, which since iOS 17 requires a RemoteServiceDiscovery channel to be established before most developer services can be reached, and that channel is a tunnel with its own protocol. So on a current iOS device, go-ios cannot do anything useful without a root daemon holding that tunnel open.

The daemon is not just a process. The global options include a tunnel info port with a default of 28100 and a tunnel info host defaulting to 127.0.0.1 or the GO_IOS_AGENT_HOST environment variable, plus device tunnel address, device tunnel RSD port, an outbound HTTP proxy URL, and a userspace tunnel port. So the tunnel daemon exposes an HTTP API on a local port by default, and the client is a separate process that talks to it. That is a sensible architecture, since the daemon is long-lived and the CLI is not, and it is also a local network service that something else on the machine could reach if the host is changed from 127.0.0.1.

The second is the NCM driver, which is a separate Go program built with cgo. The build target is:

```makefile
build:
	@$(GOEXEC) work use .
	@$(GOEXEC) build -o $(GO_IOS_BINARY_NAME) .
	@$(GOEXEC) work use ./ncm
	@CGO_ENABLED=1 $(GOEXEC) build -o $(NCM_BINARY_NAME) ./cmd/cdc-ncm/main.go
```

and the run target is sudo ./go-ncm --prometheusport=8080. So a root process gets raw USB access and creates virtual TAP network devices, and it exposes a Prometheus metrics port. The reason is in the dependency list rather than the prose: gvisor provides a user-space network stack, songgao/water provides a TUN/TAP implementation, and vishvananda/netlink is the Linux netlink interface. The project implements the iOS network control module path in user space rather than requiring a kernel driver, which is why a Go program with cgo can do it.

Putting those together: automating a current iOS device from Linux means running a root daemon that holds a tunnel and, depending on what you are doing, a second root binary that does raw USB and creates network interfaces, one of which publishes metrics on port 8080. That is a significant privilege footprint on a build host, and the project states it rather than burying it.

## go.mod is a protocol map you can read in one pass

The dependency list in this project is unusually informative, because each entry corresponds to a specific protocol or Apple format rather than to a general convenience.

The toolchain declaration is at the top: go 1.26.0 with toolchain go1.26.5. That is a current Go, and a build machine needs it.

The direct dependencies, read as a group, describe what the tool actually speaks. gvisor.dev/gvisor is Google's user-space network stack, which is how the network extension work happens without a kernel module. github.com/quic-go/quic-go is the QUIC implementation, and QUIC is the transport for the iOS 17 and later tunnel. golang.zx2c4.com/wintun is the Windows TUN driver, which is the same wintun project the README separately tells you to download as a DLL. github.com/songgao/water is a TUN/TAP implementation for the userspace side, and github.com/vishvananda/netlink is the Linux netlink interface for talking to the kernel about network devices.

Then the Apple formats. github.com/blacktop/go-macho is a Mach-O parser and github.com/blacktop/go-dwarf is a DWARF debug information reader, both listed as indirect but both clearly load-bearing, since reading a binary or its symbols off a device means parsing Mach-O and its debug info. software.sslmate.com/src/go-pkcs12 and go.mozilla.org/pkcs7 are certificate containers, and github.com/aluedeke/go-codesign is a code signing implementation. Those four together are what lets the tool handle provisioning profiles, developer identities and signed artefacts. howett.net/plist is Apple's property list parser, and it is the format behind most of the device services. github.com/pierrec/lz4 is a compression library, and github.com/tadglines/go-pkgs is an .ipa bundle reader, which is how an IPA becomes an app bundle on disk.

Then the infrastructure. github.com/elazarl/goproxy is an HTTP proxy library, which is what the httpproxy and dproxy commands are built on. github.com/google/gopacket is packet capture, which is the pcap command and the ip detection. github.com/grandcat/zeroconf is mDNS service discovery, which is how devices are found on a network. github.com/lunixbochs/struc decodes binary structures, which is how the device's plist-based lockdown responses get parsed. github.com/Masterminds/semver is version comparison, github.com/google/uuid is identifiers, and github.com/docopt/docopt-go is the CLI parser, pinned to a 2018 pseudo-version.

The test and indirect set is conventional: testify, quicktest, go-spew, go-difflib, btree, backoff, miekg/dns, golang.org/x/mod, x/sync, x/text and the standard x/crypto, x/net, x/sys, x/term, x/exp.

One small hygiene note. github.com/gorilla/websocket appears in the second require block alongside packages explicitly marked indirect, but carries no indirect comment, which means it is a direct dependency recorded in the wrong block. That is cosmetic and go mod tidy would not necessarily move it, but it is the kind of thing that suggests the file is maintained by hand.

The point of listing all of this is that a reader can tell what protocols the tool implements without reading a line of its own source. For a project whose job is talking to undocumented device services, that transparency is the documentation.

## The README's help section is generated by a perl one-liner

The command list in this README is not documentation someone maintained. It is generated, and the generator is one line of Perl in the Makefile.

The README has HTML comment markers around the help block, a begin marker before it and an end marker after it. The make target that maintains it is called readme-help:

```makefile
readme-help:
	@out=$$(mktemp /tmp/go-ios.XXXXXX); trap 'rm -f "$$out"' EXIT INT TERM; \
	  perl -pe'BEGIN{$$/=q(<!-- help begin -->)} if($$/=~s/begin/end/){<>;$$_.="\n\n```text\n".`go run . --help`."```\n\n$$/"}' README.md > "$$out" && \
		mv "$$out" README.md
```

Read it as a description of what it does rather than as code. It makes a temporary file in /tmp with a trap to remove it on exit or interrupt. Then it runs a Perl in-place editor over README.md: in a BEGIN block it captures the begin marker into the input record separator, so the file is slurped in chunks at that marker, and then on each chunk it substitutes begin with end and injects the output of go run . --help wrapped in a fenced text block between them. The result is written to the temporary file and moved over README.md.

So the command list in the README is whatever the binary's help output is at the moment someone last ran the target, and there is no way for it to drift from the code, because it is produced by the code.

That is a small piece of engineering that more CLI projects should copy. Command reference documentation is the part of a README that goes stale fastest and that readers trust most, and generating it removes the failure mode entirely. The cost is that the target requires a working Go toolchain to run, so a contributor who only wants to read the commands cannot regenerate them without building.

The generated block is also the best description of the tool's scope, because it is the authoritative list. The global options are the ones in the previous section plus two output controls: --nojson to disable JSON output and --pretty to pretty-print it. That pair is the design principle about JSON made concrete, with an escape hatch for when you want human output.

And then there are the commands, which run from the trivial to the operationally significant. activate, apps, batterycheck, date, devicename, diskspace, info, lang and list are informational. crash cp, crash ls and crash rm handle crash reports. file ls, file pull, file push and fsync handle containers. image auto, image list, image mount and image unmount handle developer images, with image auto described in the README as installing developer images automatically. profile add, profile list and profile remove handle configuration profiles. forward forwards a host port to a device. install, launch, kill, prepare and devmode handle the app lifecycle. instruments fps, instruments network and instruments notifications stream samples. ostrace streams os_trace_relay logs. pcap captures packets. ip detects the device address from a capture.

## Commands that define the scope, and the project's stated growth model

Four commands tell you what this tool is really for, and one sentence in the README tells you how it is being extended.

The commands are mobilegestalt, lockdown get, httpproxy and dproxy.

mobilegestalt queries mobilegestalt keys. On an iOS device, mobilegestalt is a plist of device identity values that apps read to decide how to behave, and it is one of the reasons an app behaves differently on your hardware than the app author expected. A tool that can query those keys is a tool that participates in device identity, and that is a capability rather than a bug.

lockdown get queries lockdown values. Lockdownd is the device service that almost everything else goes through, and it holds the device's configuration, its pairing state and a good deal of system state. Querying it is the read side of the whole device management surface, and the write side is where the commands like devicestate, devmode, memlimitoff, lang, httpproxy and prepare live.

httpproxy installs a global HTTP proxy profile, and httpproxy remove takes it away. So the tool can put a whole device behind a proxy, which is the prerequisite for inspecting or intercepting a device's own network traffic, and pcap does the same job at the packet level with gopacket. dproxy starts a debug proxy, which the README pairs with the sentence about reverse engineering Apple's own tooling.

That sentence is the growth model: use a debug proxy to reverse engineer every tool Mac OSX has, so you can contribute to go-ios or build your own. In other words, the project's stated method for growing its feature set is to observe what Apple's macOS tooling does to a device and reimplement the same operation in Go. That is why the feature list is framed as a request rather than a specification, and why the README says that if you miss something your Mac can do but go-ios cannot, you should request a feature.

Two more commands are worth naming because they show the scope reaching into accessibility and UI testing. ax runs accessibility inspector features and ax audit runs an accessibility audit, which is the kind of check an app store review performs. That is significant, because an accessibility audit on a device from Linux, in CI, without a Mac, is a capability that did not exist in this form before. And the README's most notable feature list names running XCTests including WebdriverAgent on Linux, Windows and Mac, which is the basis for the appium-ios and xcuitest topics on the repository.

So the tool is a device-services client with a CLI surface, and the CLI surface is deliberately as wide as what a Mac can do. The design principles section explains why it is a CLI at all: Go compiles static, small and fast binaries for all platforms very easily, and everything is a module so it can be used as a dependency in Go projects. The npm package exists so that the JSON output can be consumed from JavaScript, which is the other half of the JSON-everywhere principle.

## A wintun.dll copied into system32, and a committed .env

Two items in this repository deserve a check before you run it on a build machine, and neither is presented as a risk by the project.

The first is the Windows setup step. The README says that to make the tunnel work on Windows, download the latest wintun.dll from git.zx2c4.com/wintun and copy it to C:/Windows/system32.

So the documented Windows installation involves a person fetching a binary DLL from a sourceforge-hosted git repository and writing it into the system directory. The instruction does not mention a checksum, a signature, a version pin, or a mirror. It says the latest.

Two things make that more and less serious. More serious: writing to C:/Windows/system32 requires administrator rights, the file is loaded into a process that is talking to a USB device, and the recommendation is to take whatever is newest from a third-party host at the moment you set up a runner. Less serious: wintun is a well-known, long-established, actively maintained project, and its Go binding is already a declared dependency of this repository at golang.zx2c4.com/wintun. So the DLL is the native half of a dependency you are already trusting, not an unknown binary.

That is the real observation. The Go binding is versioned and recorded in go.sum, and the native half is not versioned anywhere in the project's instructions. If you are building this into a CI pipeline, the gap between a pinned Go module and an unpinned DLL is the kind of thing a supply-chain review will find, and closing it is a matter of pinning the DLL version and verifying its hash in your own provisioning rather than following the README literally.

The second item is a file called .env at the repository root, committed alongside the source. There is no .env.example visible in the top-level listing, and there is a .gitignore, so the question is why this one is tracked.

A committed .env is worth checking in this particular project rather than in general, for two reasons visible in the project itself rather than from suspicion. The tool's global options default the tunnel info host to 127.0.0.1 or the GO_IOS_AGENT_HOST environment variable, so this project uses environment variables to configure where a privileged daemon is reached, which is exactly the kind of configuration that belongs in an environment file. And the project ships commands that read device identity values, so if any of its normal operation needs a provisioning profile path, a certificate password or a team identifier, those are the values an .env would hold.

The README does not mention the file, so its contents and its purpose are not documented. Before running this on a machine you care about, read it. That is a thirty-second check and it is the kind of check a careful evaluator does on any project with a tracked .env.

There is also a file called bla.md at the root, which nothing in the README references. That is the kind of stray note that accumulates in a repository nobody has time to tidy, and it is worth a glance for the same reason: undocumented files in a security-adjacent tool are worth reading rather than assuming are harmless.

## Two modules in a workspace, cgo for one, and go vet for linting

The build setup has a small structure that the README does not describe, and it is worth understanding before you build from source.

The repository root contains go.mod and go.sum for the main module, and also go.work and go.work.sum, which is a Go workspace file. There is a separate ncm/ directory with its own module, and pool-data/ which appears to be a data directory. So this is two modules in one workspace, and the workspace file is what lets a single go command see both.

The Makefile activates them in sequence. The build target runs go work use . on the root module, builds the ios binary, then runs go work use ./ncm and builds the NCM binary from cmd/cdc-ncm/main.go with CGO_ENABLED=1. So the driver is a separate module with a separate binary name, go-ncm, and it is the one that needs cgo.

The cgo requirement is the interesting constraint, because cgo means a C toolchain on the build machine and it means the resulting binary is not static. That sits against the project's first design principle, which is compiling static, small and fast binaries for all platforms easily. The main ios binary satisfies that. The NCM driver does not, and it is a Linux-only, root-only, USB-touching component, so the tension is contained.

Cross-compilation is handled in a way that is worth noting because it is a common Go build annoyance solved compactly. The Makefile defines empty OS and ARCH variables, comments that they should be defined only if compiling for a different system, and then builds a GOEXEC prefix by prepending GO<var>=<value> for each non-empty variable. So make build OS=windows produces go with GOOS=windows in front of it, without any conditional logic in the recipe.

The development targets are thin. lint is go vet ./..., which is the minimum rather than a real lint configuration; there is no golangci-lint, no staticcheck and no revive in the visible setup. setup installs the git hooks by running git config core.hooksPath .githooks, which is the same pattern as several other projects in this set: the hooks live in a tracked directory rather than in .git/hooks, so a clone does not activate them until someone runs the target, and CONTRIBUTING.md is at the root to tell them to.

The tests are in the root rather than in a tests/ directory: main_test.go, main_extra_test.go, command_registry_test.go, and a set of cmd_device_*_test.go files that pair with the flat cmd_device_*.go source files. There is also a test/ directory and testdata/, so both arrangements are in use.

The flat source layout is the most unusual thing. Roughly twenty-five cmd_device_*.go files sit at the repository root next to main.go, cli_setup.go, cli_device_resolution.go and command_registry.go, with the real packages in cmd/, internal/, ios/ and restapi/. A Go project of this size would normally put the command implementations under cmd/. Having them at the root keeps the source file next to its test file, which is pleasant in an editor, and it is the kind of choice a single author makes and keeps.

## Three releases in a day, commercial users, and a REST API marked experimental

The release history and the stated adoption tell you what kind of project this is, and both are more reassuring than the version numbers suggest.

The three most recent releases are v1.3.0 on 2026-08-10, v1.3.1 on 2026-08-11 at 14:41, and v1.3.2 on 2026-08-11 at 16:41. Three releases in about twenty-seven hours, and the last push to the repository was on 2026-09-28. So this is an actively released project on a minor-version cadence, and the v1.3.x sequence looks like a feature plus two fixes in a day, which is what a bug report from a commercial user produces.

The README names two of those users, and they are the right names to hear. It says a few companies including headspin.io and Sauce Labs will use or are using go-ios. Both are mobile testing infrastructure vendors, and both run device farms at scale. That is the strongest single signal in this repository: the tool is used by people whose business is running thousands of iOS devices, and a tool that only worked on a developer's Mac would not survive contact with them.

It is also a signal about the maintenance burden. Sauce Labs and HeadSpin have to keep it working across iOS point releases, which is the workload the project exists to absorb, and the topic list includes hacktoberfest2022 and hacktoberfest2023, so the project has also been a route for outside contributors to pick up gaps.

The version number being at 1.3 rather than 5 or 0.2 is a mild signal about maturity expectations. A tool at 1.3 that automates a platform whose developer surface changes annually is going to break on a new iOS release, and the maintainer's own framing acknowledges that by asking for feature requests for anything macOS can do that go-ios cannot.

The REST API is the newest surface and it is labelled experimental in the README, with a link into the restapi/ directory. The restapi/ directory is in the top-level listing alongside npm_publish/, which is the package build for the npm distribution, and usbmuxdbuild/, which is presumably a build helper for the usbmuxd component.

For an evaluator, the layering is the right shape and the labelling is honest. The CLI with JSON output is the stable interface, and it is what the npm package and the REST API are both built on top of. The JSON-everywhere design principle is what makes that possible, and it is also what lets a Python, Java or Go team drive the tool without writing Go. So the REST layer being experimental is a low-risk thing to depend on indirectly and a high-risk thing to depend on directly, and the README is clear about which it is.

One last item for completeness: there is a CHANGELOG.md, CONTRIBUTING.md, SECURITY.md is not in the visible top-level listing, AGENTS.md and CLAUDE.md at the root, a configure file, and a .githooks directory. The presence of AGENTS.md and CLAUDE.md in a Go device-automation tool follows the same pattern seen elsewhere in this set of repositories, and its presence alongside a CONTRIBUTING.md suggests the project is being worked on with tool assistance rather than only with a text editor.

## Conclusion

Use go-ios if you run iOS device automation on infrastructure that is not a Mac, because that is the whole reason it exists and the alternative is a Mac in your CI with a licensing and utilisation problem. Do not adopt it on a workstation where a root-owned daemon doing raw USB access and creating TAP network devices is not acceptable, because the Makefile's own comment says cdc-ncm must be run with sudo on Linux for USB access and virtual TAP setup, and the tunnel daemon for iOS 17 and later is also started with sudo. Verify five things. Whether your devices are on iOS 17 or later, since older devices use a different path and the tunnel is a requirement rather than an optimisation. Whether you can distribute a wintun.dll into C:/Windows/system32 on every Windows runner, since the README gives that as a manual step with no checksum mentioned. What is in the committed .env at the repository root before you run anything, in a tool whose stated scope includes querying mobilegestalt keys. Whether the experimental REST API in restapi/ is something you want, because the CLI's JSON-everywhere design is the stable interface and the REST layer is explicitly labelled experimental. And whether you need a Mac for the parts this tool does not cover, because the README invites feature requests for anything macOS can do that go-ios cannot, which is an admission of the remaining gap. The deciding fact is that go-ios is the tooling layer under cross-platform iOS testing, adopted by commercial vendors, and its cost is a privileged daemon on your build host.

## FAQ

### How do I install and start go-ios?

Run npm install -g go-ios, or install Go and run go build, which produces a static binary named ios. For iOS 17 and later devices you must run sudo ios tunnel start first, which starts a tunnel daemon. On Windows you additionally need to download the latest wintun.dll from git.zx2c4.com/wintun and copy it into C:/Windows/system32. Then run ios --help, ios help <command> or ios <command> --help.

### What does go-ios need in terms of privileges?

Two privileged components on Linux. The tunnel daemon for iOS 17 and later is started with sudo ios tunnel start and exposes a tunnel info API on port 28100 by default. Separately, the cdc-ncm driver must be run with sudo for raw USB access and for setting up virtual TAP network devices, and it takes a Prometheus metrics port. The Makefile's own comment says both binaries are built by make build and that the driver is run with sudo.

### Which platforms does go-ios support?

Linux, Windows and macOS, which is the project's stated goal: a stable and production ready open source solution to automate iOS devices on all three, compiled as static Go binaries. On Windows it additionally needs wintun.dll in the system directory, and the README recommends using gopacket-based packet capture, which is what the pcap command and the ip command use.

### What can go-ios actually do to a device?

Install apps from an IPA or an .app folder, run XCTests including WebdriverAgent, start and stop apps, kill apps by bundle ID or PID, list and pull and push files in app, group, temp and crash containers, list and copy and remove crash reports, mount developer images, install and remove configuration profiles, install and remove a global HTTP proxy, forward a host port, capture packets, stream os_trace_relay logs, stream frames-per-second and network samples, run an accessibility audit, and set device condition profiles, language and developer mode.

### Why is all the go-ios output JSON?

It is the second of the project's three design principles: all output as JSON so you can easily use go-ios from any other programming language. The CLI provides --nojson to disable it and --pretty to pretty-print it. Combined with the third principle, that everything is a module so it can be used as a Go dependency, and the npm package and the experimental REST API in restapi/ are both built on that JSON interface.

### How is the go-ios command reference kept up to date?

It is generated. The README wraps the help block in HTML comment markers, and the Makefile has a readme-help target that runs a Perl in-place editor over README.md to inject the output of go run . --help between those markers. So the command list in the README is produced by the binary and cannot drift from the code, though regenerating it requires a working Go toolchain.

## Sources

- [danielpaulus/go-ios on GitHub](https://github.com/danielpaulus/go-ios)
- [Issues](https://github.com/danielpaulus/go-ios/issues)
- [License: MIT](https://github.com/danielpaulus/go-ios/blob/main/LICENSE)
- [README](https://github.com/danielpaulus/go-ios/blob/main/README.md)
- [Releases](https://github.com/danielpaulus/go-ios/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/danielpaulus-go-ios
