Skip to content

Add ADB reverse port forwarding support#305

Open
rmarinho wants to merge 1 commit intomainfrom
feature/adb-reverse-port
Open

Add ADB reverse port forwarding support#305
rmarinho wants to merge 1 commit intomainfrom
feature/adb-reverse-port

Conversation

@rmarinho
Copy link
Member

@rmarinho rmarinho commented Mar 13, 2026

Summary

Add reverse port forwarding APIs to AdbRunner, enabling the MAUI DevTools CLI to manage hot-reload tunnels without going through ServiceHub.

Closes #303

Context

Reverse port forwarding (adb reverse) is used across three separate codebases today, each with its own implementation:

1. Visual Studio (ClientTools.Platform) — ADB Wire Protocol

External/android-platform-support/src/Mono.AndroidTools/AdbServer.cs
  • Uses raw ADB wire protocol commands via TransportRunCommandWithStatus:
    • ReversePort()reverse:forward:tcp:{remote};tcp:{local}
    • KillReverse()reverse:killforward:{proto}:{port}
  • Consumer: AndroidDebugLaunchProvider.CreateLocalTunnelsAsync() sets up tunnels on deploy, RemoveLocalTunnelsAsync() tears them down on session end
  • Complex: requires managing raw ADB server sockets, transport selection, status parsing

2. VS Code Extension (vscode-maui) — ServiceHub RPC

src/Microsoft.VisualStudio.Maui/services/maui/IAndroidDeviceManager.ts
  • Calls the same AdbServer via ServiceHub C# host:
    • reversePort(deviceId, remoteProtocol, remotePort, localProtocol, localPort)
    • killReverse(deviceId, remoteProtocol, remotePort)
  • Consumer: MauiAndroidPlatform.createLocalTunnels() / cleanUpDebugSession()
  • Requires ServiceHub infrastructure (C# host process, JSON-RPC)

3. Manual CLI — No C# implementation

  • dotnet/android docs reference adb reverse tcp:9000 tcp:9001 for tracing
  • No programmatic wrapper exists

The Problem

Each IDE reimplements the same ADB operations. Bug fixes or behavior changes must be duplicated. The wire protocol approach in AdbServer.cs is powerful but complex — it manages raw sockets, transport negotiation, and status parsing that the adb CLI handles automatically.

What This PR Does

Adds a shared, CLI-based implementation in android-tools that all consumers can use:

// Set up reverse tunnel (Hot Reload, tracing, etc.)
await adbRunner.ReversePortAsync("emulator-5554", remotePort: 5000, localPort: 5000);

// Remove specific tunnel
await adbRunner.RemoveReversePortAsync("emulator-5554", remotePort: 5000);

// Remove all tunnels (cleanup on crash/stale state)
await adbRunner.RemoveAllReversePortsAsync("emulator-5554");

// List active tunnels (diagnostics)
var rules = await adbRunner.ListReversePortsAsync("emulator-5554");

How This Simplifies Each Consumer

Visual Studio (future)

Before: AdbServer.ReversePort() using raw wire protocol in android-platform-support
After: Can switch to AdbRunner.ReversePortAsync() from the Xamarin.Android.Tools.AndroidSdk NuGet (already consumed via submodule in dotnet/android). This eliminates the need for raw socket management — the adb CLI handles transport selection, protocol negotiation, and error reporting.

The AdbServer.cs wire protocol code can be gradually replaced, method by method, reducing the maintenance surface in android-platform-support.

VS Code Extension

Before: TypeScript → ServiceHub JSON-RPC → C# AdbServer.ReversePort() → ADB wire protocol
After: TypeScript → maui android adb reverse --device <serial> --remote <port> --local <port>AdbRunner.ReversePortAsync()adb CLI

Removes the ServiceHub dependency for this operation entirely. The CLI command will be added downstream in dotnet/maui DevTools CLI.

dotnet/android

Can use AdbRunner.ReversePortAsync() directly in MSBuild tasks if tracing/profiling setup is ever automated (currently manual per docs).

New API Surface

All methods are virtual for testability, follow existing AdbRunner patterns (ProcessUtils.StartProcess, ThrowIfFailed), and include parameter validation.

AdbRunner methods

Method ADB Command Purpose
ReversePortAsync(serial, remotePort, localPort, ct) adb -s <serial> reverse tcp:<remote> tcp:<local> Set up reverse port forwarding
RemoveReversePortAsync(serial, remotePort, ct) adb -s <serial> reverse --remove tcp:<remote> Remove a specific forwarding rule
RemoveAllReversePortsAsync(serial, ct) adb -s <serial> reverse --remove-all Remove all forwarding rules
ListReversePortsAsync(serial, ct) adb -s <serial> reverse --list List active forwarding rules

New type: AdbReversePortRule

public class AdbReversePortRule
{
    public string Remote { get; init; }  // e.g. "tcp:5000"
    public string Local { get; init; }   // e.g. "tcp:5000"
}

Internal: ParseReverseListOutput

Parses adb reverse --list output format: (reverse) tcp:5000 tcp:5000

Key Design Decisions

  1. CLI-based, not wire protocol — Uses adb reverse CLI commands instead of raw ADB server wire protocol. The CLI handles transport selection, error formatting, and protocol negotiation. Simpler to maintain and test, and functionally equivalent.

  2. Port parameters as int — Since ADB reverse only supports TCP, we take int remotePort/localPort and build the tcp:N spec internally. Matches the simplified overload in AdbServer.ReversePort(device, remotePort, localPort, ct).

  3. RemoveAllReversePortsAsync — Beyond what VS/VS Code do today, but useful for cleanup scenarios (session crash, stale tunnels from a previous debug session).

  4. ListReversePortsAsync — Enables diagnostic/status reporting. Neither VS nor VS Code expose this today, but it is valuable for the CLI experience (maui android adb reverse --list --json).

Tests

14 new tests, all passing (252 total, 0 failures):

  • ParseReverseListOutput: single rule, multiple rules, empty output, no lines, non-reverse lines ignored, malformed lines, different remote/local ports
  • Parameter validation: empty serial, zero port, negative port, port > 65535 for all 4 methods

Migration Path

Phase 1 (this PR): Shared API in android-tools
Phase 2: MAUI DevTools CLI exposes `maui android adb reverse` command
Phase 3: vscode-maui calls CLI instead of ServiceHub for reverse port
Phase 4: Visual Studio can optionally switch from AdbServer wire protocol
         to AdbRunner CLI (reduces android-platform-support complexity)

Opportunity for dotnet/android

Today, documentation instructs users to manually set up reverse port forwarding for tracing and profiling scenarios:

# From Documentation/guides/tracing.md — users must run this by hand:
adb reverse tcp:9000 tcp:9001
adb shell setprop debug.mono.profile '127.0.0.1:9000,suspend,connect'

With AdbRunner.ReversePortAsync() now available in the same package that dotnet/android already consumes via submodule (external/xamarin-android-tools), there is an opportunity to automate this in MSBuild tasks. For example, a hypothetical SetupTracingTunnel task could:

// In a future MSBuild task (not part of this PR):
var adbRunner = new AdbRunner(adbPath);
await adbRunner.ReversePortAsync(deviceSerial, remotePort: 9000, localPort: 9001, cancellationToken);
// Then set the debug property via RunShellCommandAsync...

This would eliminate a manual step from the profiling workflow and reduce the chance of user error.

@jonathanpeppers — Would automating for tracing/profiling in MSBuild tasks be useful? The API is here if so, but this PR does not depend on that decision — the primary consumers are the IDE hot-reload tunnels (VS and VS Code).

Related

Add ReversePortAsync, RemoveReversePortAsync, RemoveAllReversePortsAsync,
and ListReversePortsAsync methods to AdbRunner for managing reverse port
forwarding rules. These APIs enable the MAUI DevTools CLI to manage
hot-reload tunnels without going through ServiceHub.

New type AdbReversePortRule represents entries from 'adb reverse --list'.
Internal ParseReverseListOutput handles parsing the output format.

Includes 14 new tests covering parsing and parameter validation.

Closes #303

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings March 13, 2026 09:20
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds reverse port forwarding support to the Android SDK runner layer so downstream tooling (MAUI DevTools CLI / VS Code MAUI extension) can manage Hot Reload tunnels directly via library APIs.

Changes:

  • Added AdbRunner APIs for adb reverse operations (add/remove/remove-all/list) plus parsing for reverse --list output.
  • Introduced new public model type AdbReversePortRule to represent a listed reverse rule.
  • Updated PublicAPI unshipped files and added unit tests for parsing and parameter validation.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.cs Adds tests for reverse list parsing and basic parameter validation for new APIs
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs Implements new reverse port forwarding methods and output parsing helper
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbReversePortRule.cs Adds public DTO for reverse port rules
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt Declares new public API surface for netstandard2.0
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txt Declares new public API surface for net10.0

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +780 to +786
[Test]
public void ReversePortAsync_PortAbove65535_ThrowsArgumentOutOfRange ()
{
var runner = new AdbRunner ("/fake/sdk/platform-tools/adb");
Assert.ThrowsAsync<System.ArgumentOutOfRangeException> (
async () => await runner.ReversePortAsync ("emulator-5554", 70000, 5000));
}
using var stderr = new StringWriter ();
var psi = ProcessUtils.CreateProcessStartInfo (adbPath, "-s", serial, "reverse", "--list");
var exitCode = await ProcessUtils.StartProcess (psi, stdout, stderr, cancellationToken, environmentVariables).ConfigureAwait (false);
ProcessUtils.ThrowIfFailed (exitCode, $"adb -s {serial} reverse --list", stderr);
Comment on lines +149 to +153
Xamarin.Android.Tools.AdbReversePortRule.AdbReversePortRule() -> void
Xamarin.Android.Tools.AdbReversePortRule.Local.get -> string!
Xamarin.Android.Tools.AdbReversePortRule.Local.init -> void
Xamarin.Android.Tools.AdbReversePortRule.Remote.get -> string!
Xamarin.Android.Tools.AdbReversePortRule.Remote.init -> void
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I question the API design:

public class AdbReversePortRule
{
    public string Remote { get; init; }  // e.g. "tcp:5000"
    public string Local { get; init; }   // e.g. "tcp:5000"
}

C# is strongly typed, we shouldn't be using strings like tcp:5000.

We should have an enum as a separate property to specify if it is tcp (and fill in the other options in the enum).

Then we should have an int property for the port.

Can't you also do adb forward?

So should this be:

public record AdbPortAndProtocol (/* this is the enum*/ AdbProtocol Protocol, int Port);

public record AdbReverseRule (AdbPortAndProtocol Remote, AdbPortAndProtocol Local);

public record AdbForwardingRule (AdbPortAndProtocol Remote, AdbPortAndProtocol Local);

But then I don't know if I like having two records for reverse & forward. Maybe it should be a single record + another enum that says if it is Forward/Reverse.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add ADB reverse port forwarding support

3 participants