Unverified Commit 33f79950 authored by Satsuki Ueno's avatar Satsuki Ueno Committed by GitHub

fuchsia_remote_debug_protocol allows open port on remote device (#63996)

* fuchsia_remote_debug_protocol allows open port on remote device

Allows defining a port forwarding function for which the accessible
port is not on the host device. Examples include tunneling solutions
where a tunneling program on the same device as the Dart VM exposes
an open port through which it tunnels connections to the VM.

* Move ssh-specific comment to SshPortForwarder
parent 5a0e0978
...@@ -25,6 +25,9 @@ class _DummyPortForwarder implements PortForwarder { ...@@ -25,6 +25,9 @@ class _DummyPortForwarder implements PortForwarder {
@override @override
int get remotePort => _remotePort; int get remotePort => _remotePort;
@override
String get openPortAddress => InternetAddress.loopbackIPv4.address;
@override @override
Future<void> stop() async { } Future<void> stop() async { }
} }
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:core'; import 'dart:io';
/// Determines whether `address` is a valid IPv6 or IPv4 address. /// Determines whether `address` is a valid IPv6 or IPv4 address.
/// ///
...@@ -17,7 +17,7 @@ void validateAddress(String address) { ...@@ -17,7 +17,7 @@ void validateAddress(String address) {
/// Returns true if `address` is a valid IPv6 address. /// Returns true if `address` is a valid IPv6 address.
bool isIpV6Address(String address) { bool isIpV6Address(String address) {
try { try {
Uri.parseIPv6Address(address); InternetAddress(address, type:InternetAddressType.IPv6);
return true; return true;
} on FormatException { } on FormatException {
return false; return false;
...@@ -27,7 +27,7 @@ bool isIpV6Address(String address) { ...@@ -27,7 +27,7 @@ bool isIpV6Address(String address) {
/// Returns true if `address` is a valid IPv4 address. /// Returns true if `address` is a valid IPv4 address.
bool isIpV4Address(String address) { bool isIpV4Address(String address) {
try { try {
Uri.parseIPv4Address(address); InternetAddress(address, type:InternetAddressType.IPv4);
return true; return true;
} on FormatException { } on FormatException {
return false; return false;
......
...@@ -103,13 +103,13 @@ class DartVmEvent { ...@@ -103,13 +103,13 @@ class DartVmEvent {
/// This class can be connected to several instances of the Fuchsia device's /// This class can be connected to several instances of the Fuchsia device's
/// Dart VM at any given time. /// Dart VM at any given time.
class FuchsiaRemoteConnection { class FuchsiaRemoteConnection {
FuchsiaRemoteConnection._(this._useIpV6Loopback, this._sshCommandRunner) FuchsiaRemoteConnection._(this._useIpV6, this._sshCommandRunner)
: _pollDartVms = false; : _pollDartVms = false;
bool _pollDartVms; bool _pollDartVms;
final List<PortForwarder> _forwardedVmServicePorts = <PortForwarder>[]; final List<PortForwarder> _forwardedVmServicePorts = <PortForwarder>[];
final SshCommandRunner _sshCommandRunner; final SshCommandRunner _sshCommandRunner;
final bool _useIpV6Loopback; final bool _useIpV6;
/// A mapping of Dart VM ports (as seen on the target machine), to /// A mapping of Dart VM ports (as seen on the target machine), to
/// [PortForwarder] instances mapping from the local machine to the target /// [PortForwarder] instances mapping from the local machine to the target
...@@ -126,15 +126,15 @@ class FuchsiaRemoteConnection { ...@@ -126,15 +126,15 @@ class FuchsiaRemoteConnection {
StreamController<DartVmEvent>(); StreamController<DartVmEvent>();
/// VM service cache to avoid repeating handshakes across function /// VM service cache to avoid repeating handshakes across function
/// calls. Keys a forwarded port to a DartVm connection instance. /// calls. Keys a URI to a DartVm connection instance.
final Map<int, DartVm> _dartVmCache = <int, DartVm>{}; final Map<Uri, DartVm> _dartVmCache = <Uri, DartVm>{};
/// Same as [FuchsiaRemoteConnection.connect] albeit with a provided /// Same as [FuchsiaRemoteConnection.connect] albeit with a provided
/// [SshCommandRunner] instance. /// [SshCommandRunner] instance.
static Future<FuchsiaRemoteConnection> connectWithSshCommandRunner(SshCommandRunner commandRunner) async { static Future<FuchsiaRemoteConnection> connectWithSshCommandRunner(SshCommandRunner commandRunner) async {
final FuchsiaRemoteConnection connection = FuchsiaRemoteConnection._( final FuchsiaRemoteConnection connection = FuchsiaRemoteConnection._(
isIpV6Address(commandRunner.address), commandRunner); isIpV6Address(commandRunner.address), commandRunner);
await connection._forwardLocalPortsToDeviceServicePorts(); await connection._forwardOpenPortsToDeviceServicePorts();
Stream<DartVmEvent> dartVmStream() { Stream<DartVmEvent> dartVmStream() {
Future<void> listen() async { Future<void> listen() async {
...@@ -224,14 +224,16 @@ class FuchsiaRemoteConnection { ...@@ -224,14 +224,16 @@ class FuchsiaRemoteConnection {
for (final PortForwarder pf in _forwardedVmServicePorts) { for (final PortForwarder pf in _forwardedVmServicePorts) {
// Closes VM service first to ensure that the connection is closed cleanly // Closes VM service first to ensure that the connection is closed cleanly
// on the target before shutting down the forwarding itself. // on the target before shutting down the forwarding itself.
final DartVm vmService = _dartVmCache[pf.port]; final Uri uri = _getDartVmUri(pf);
_dartVmCache[pf.port] = null; final DartVm vmService = _dartVmCache[uri];
_dartVmCache[uri] = null;
await vmService?.stop(); await vmService?.stop();
await pf.stop(); await pf.stop();
} }
for (final PortForwarder pf in _dartVmPortMap.values) { for (final PortForwarder pf in _dartVmPortMap.values) {
final DartVm vmService = _dartVmCache[pf.port]; final Uri uri = _getDartVmUri(pf);
_dartVmCache[pf.port] = null; final DartVm vmService = _dartVmCache[uri];
_dartVmCache[uri] = null;
await vmService?.stop(); await vmService?.stop();
await pf.stop(); await pf.stop();
} }
...@@ -258,7 +260,7 @@ class FuchsiaRemoteConnection { ...@@ -258,7 +260,7 @@ class FuchsiaRemoteConnection {
if (event.eventType == DartVmEventType.started) { if (event.eventType == DartVmEventType.started) {
_log.fine('New VM found on port: ${event.servicePort}. Searching ' _log.fine('New VM found on port: ${event.servicePort}. Searching '
'for Isolate: $pattern'); 'for Isolate: $pattern');
final DartVm vmService = await _getDartVm(event.uri.port, final DartVm vmService = await _getDartVm(event.uri,
timeout: _kDartVmConnectionTimeout); timeout: _kDartVmConnectionTimeout);
// If the VM service is null, set the result to the empty list. // If the VM service is null, set the result to the empty list.
final List<IsolateRef> result = await vmService final List<IsolateRef> result = await vmService
...@@ -307,7 +309,7 @@ class FuchsiaRemoteConnection { ...@@ -307,7 +309,7 @@ class FuchsiaRemoteConnection {
<Future<List<IsolateRef>>>[]; <Future<List<IsolateRef>>>[];
for (final PortForwarder fp in _dartVmPortMap.values) { for (final PortForwarder fp in _dartVmPortMap.values) {
final DartVm vmService = final DartVm vmService =
await _getDartVm(fp.port, timeout: vmConnectionTimeout); await _getDartVm(_getDartVmUri(fp), timeout: vmConnectionTimeout);
if (vmService == null) { if (vmService == null) {
continue; continue;
} }
...@@ -385,13 +387,13 @@ class FuchsiaRemoteConnection { ...@@ -385,13 +387,13 @@ class FuchsiaRemoteConnection {
_dartVmEventController.add(DartVmEvent._( _dartVmEventController.add(DartVmEvent._(
eventType: DartVmEventType.stopped, eventType: DartVmEventType.stopped,
servicePort: pf.remotePort, servicePort: pf.remotePort,
uri: _getDartVmUri(pf.port), uri: _getDartVmUri(pf),
)); ));
} }
} }
for (final PortForwarder pf in _dartVmPortMap.values) { for (final PortForwarder pf in _dartVmPortMap.values) {
final DartVm service = await _getDartVm(pf.port); final DartVm service = await _getDartVm(_getDartVmUri(pf));
if (service == null) { if (service == null) {
await shutDownPortForwarder(pf); await shutDownPortForwarder(pf);
} else { } else {
...@@ -402,15 +404,14 @@ class FuchsiaRemoteConnection { ...@@ -402,15 +404,14 @@ class FuchsiaRemoteConnection {
return result; return result;
} }
Uri _getDartVmUri(int port) { Uri _getDartVmUri(PortForwarder pf) {
// While the IPv4 loopback can be used for the initial port forwarding String addr;
// (see [PortForwarder.start]), the address is actually bound to the IPv6 if (pf.openPortAddress == null) {
// loopback device, so connecting to the IPv4 loopback would fail when the addr = _useIpV6 ? '[$_ipv6Loopback]' : _ipv4Loopback;
// target address is IPv6 link-local. } else {
final String addr = _useIpV6Loopback addr = _useIpV6 ? '[${pf.openPortAddress}]' : pf.openPortAddress;
? 'http://[$_ipv6Loopback]:$port' }
: 'http://$_ipv4Loopback:$port'; final Uri uri = Uri.http('$addr:${pf.port}', '/');
final Uri uri = Uri.parse(addr);
return uri; return uri;
} }
...@@ -419,17 +420,16 @@ class FuchsiaRemoteConnection { ...@@ -419,17 +420,16 @@ class FuchsiaRemoteConnection {
/// Returns null if either there is an [HttpException] or a /// Returns null if either there is an [HttpException] or a
/// [TimeoutException], else a [DartVm] instance. /// [TimeoutException], else a [DartVm] instance.
Future<DartVm> _getDartVm( Future<DartVm> _getDartVm(
int port, { Uri uri, {
Duration timeout = _kDartVmConnectionTimeout, Duration timeout = _kDartVmConnectionTimeout,
}) async { }) async {
if (!_dartVmCache.containsKey(port)) { if (!_dartVmCache.containsKey(uri)) {
// When raising an HttpException this means that there is no instance of // When raising an HttpException this means that there is no instance of
// the Dart VM to communicate with. The TimeoutException is raised when // the Dart VM to communicate with. The TimeoutException is raised when
// the Dart VM instance is shut down in the middle of communicating. // the Dart VM instance is shut down in the middle of communicating.
try { try {
final DartVm dartVm = final DartVm dartVm = await DartVm.connect(uri, timeout: timeout);
await DartVm.connect(_getDartVmUri(port), timeout: timeout); _dartVmCache[uri] = dartVm;
_dartVmCache[port] = dartVm;
} on HttpException { } on HttpException {
_log.warning('HTTP Exception encountered connecting to new VM'); _log.warning('HTTP Exception encountered connecting to new VM');
return null; return null;
...@@ -438,7 +438,7 @@ class FuchsiaRemoteConnection { ...@@ -438,7 +438,7 @@ class FuchsiaRemoteConnection {
return null; return null;
} }
} }
return _dartVmCache[port]; return _dartVmCache[uri];
} }
/// Checks for changes in the list of Dart VM instances. /// Checks for changes in the list of Dart VM instances.
...@@ -460,7 +460,7 @@ class FuchsiaRemoteConnection { ...@@ -460,7 +460,7 @@ class FuchsiaRemoteConnection {
_dartVmEventController.add(DartVmEvent._( _dartVmEventController.add(DartVmEvent._(
eventType: DartVmEventType.started, eventType: DartVmEventType.started,
servicePort: servicePort, servicePort: servicePort,
uri: _getDartVmUri(_dartVmPortMap[servicePort].port), uri: _getDartVmUri(_dartVmPortMap[servicePort]),
)); ));
} }
} }
...@@ -482,11 +482,11 @@ class FuchsiaRemoteConnection { ...@@ -482,11 +482,11 @@ class FuchsiaRemoteConnection {
); );
} }
/// Forwards a series of local device ports to the remote device. /// Forwards a series of open ports to the remote device.
/// ///
/// When this function is run, all existing forwarded ports and connections /// When this function is run, all existing forwarded ports and connections
/// are reset by way of [stop]. /// are reset by way of [stop].
Future<void> _forwardLocalPortsToDeviceServicePorts() async { Future<void> _forwardOpenPortsToDeviceServicePorts() async {
await stop(); await stop();
final List<int> servicePorts = await getDeviceServicePorts(); final List<int> servicePorts = await getDeviceServicePorts();
final List<PortForwarder> forwardedVmServicePorts = final List<PortForwarder> forwardedVmServicePorts =
...@@ -548,9 +548,13 @@ class FuchsiaRemoteConnection { ...@@ -548,9 +548,13 @@ class FuchsiaRemoteConnection {
/// ///
/// To shut down a port forwarder you must call the [stop] function. /// To shut down a port forwarder you must call the [stop] function.
abstract class PortForwarder { abstract class PortForwarder {
/// Determines the port which is being forwarded from the local machine. /// Determines the port which is being forwarded.
int get port; int get port;
/// The address on which the open port is accessible. Defaults to null to
/// indicate local loopback.
String get openPortAddress => null;
/// The destination port on the other end of the port forwarding tunnel. /// The destination port on the other end of the port forwarding tunnel.
int get remotePort; int get remotePort;
...@@ -581,6 +585,9 @@ class _SshPortForwarder implements PortForwarder { ...@@ -581,6 +585,9 @@ class _SshPortForwarder implements PortForwarder {
@override @override
int get port => _localSocket.port; int get port => _localSocket.port;
@override
String get openPortAddress => _ipV6 ? _ipv6Loopback : _ipv4Loopback;
@override @override
int get remotePort => _remotePort; int get remotePort => _remotePort;
...@@ -602,8 +609,9 @@ class _SshPortForwarder implements PortForwarder { ...@@ -602,8 +609,9 @@ class _SshPortForwarder implements PortForwarder {
// TODO(awdavies): The square-bracket enclosure for using the IPv6 loopback // TODO(awdavies): The square-bracket enclosure for using the IPv6 loopback
// didn't appear to work, but when assigning to the IPv4 loopback device, // didn't appear to work, but when assigning to the IPv4 loopback device,
// netstat shows that the local port is actually being used on the IPv6 // netstat shows that the local port is actually being used on the IPv6
// loopback (::1). While this can be used for forwarding to the destination // loopback (::1). Therefore, while the IPv4 loopback can be used for
// IPv6 interface, it cannot be used to connect to a websocket. // forwarding to the destination IPv6 interface, when connecting to the
// websocket, the IPV6 loopback should be used.
final String formattedForwardingUrl = final String formattedForwardingUrl =
'${localSocket.port}:$_ipv4Loopback:$remotePort'; '${localSocket.port}:$_ipv4Loopback:$remotePort';
final String targetAddress = final String targetAddress =
......
...@@ -31,23 +31,6 @@ void main() { ...@@ -31,23 +31,6 @@ void main() {
const String interface = 'eno1'; const String interface = 'eno1';
when(mockRunner.address).thenReturn(address); when(mockRunner.address).thenReturn(address);
when(mockRunner.interface).thenReturn(interface); when(mockRunner.interface).thenReturn(interface);
forwardedPorts = <MockPortForwarder>[];
int port = 0;
Future<PortForwarder> mockPortForwardingFunction(
String address,
int remotePort, [
String interface = '',
String configFile,
]) {
return Future<PortForwarder>(() {
final MockPortForwarder pf = MockPortForwarder();
forwardedPorts.add(pf);
when(pf.port).thenReturn(port++);
when(pf.remotePort).thenReturn(remotePort);
return pf;
});
}
final List<Map<String, dynamic>> flutterViewCannedResponses = final List<Map<String, dynamic>> flutterViewCannedResponses =
<Map<String, dynamic>>[ <Map<String, dynamic>>[
<String, dynamic>{ <String, dynamic>{
...@@ -90,6 +73,7 @@ void main() { ...@@ -90,6 +73,7 @@ void main() {
}, },
]; ];
forwardedPorts = <MockPortForwarder>[];
mockPeerConnections = <MockPeer>[]; mockPeerConnections = <MockPeer>[];
uriConnections = <Uri>[]; uriConnections = <Uri>[];
Future<json_rpc.Peer> mockVmConnectionFunction( Future<json_rpc.Peer> mockVmConnectionFunction(
...@@ -109,7 +93,6 @@ void main() { ...@@ -109,7 +93,6 @@ void main() {
}); });
} }
fuchsiaPortForwardingFunction = mockPortForwardingFunction;
fuchsiaVmServiceConnectionFunction = mockVmConnectionFunction; fuchsiaVmServiceConnectionFunction = mockVmConnectionFunction;
}); });
...@@ -121,6 +104,86 @@ void main() { ...@@ -121,6 +104,86 @@ void main() {
}); });
test('end-to-end with three vm connections and flutter view query', () async { test('end-to-end with three vm connections and flutter view query', () async {
int port = 0;
Future<PortForwarder> mockPortForwardingFunction(
String address,
int remotePort, [
String interface = '',
String configFile,
]) {
return Future<PortForwarder>(() {
final MockPortForwarder pf = MockPortForwarder();
forwardedPorts.add(pf);
when(pf.port).thenReturn(port++);
when(pf.remotePort).thenReturn(remotePort);
return pf;
});
}
fuchsiaPortForwardingFunction = mockPortForwardingFunction;
final FuchsiaRemoteConnection connection =
await FuchsiaRemoteConnection.connectWithSshCommandRunner(mockRunner);
// [mockPortForwardingFunction] will have returned three different
// forwarded ports, incrementing the port each time by one. (Just a sanity
// check that the forwarding port was called).
expect(forwardedPorts.length, 3);
expect(forwardedPorts[0].remotePort, 123);
expect(forwardedPorts[1].remotePort, 456);
expect(forwardedPorts[2].remotePort, 789);
expect(forwardedPorts[0].port, 0);
expect(forwardedPorts[1].port, 1);
expect(forwardedPorts[2].port, 2);
// VMs should be accessed via localhost ports given by
// [mockPortForwardingFunction].
expect(uriConnections[0],
Uri(scheme:'ws', host:'[::1]', port:0, path:'/ws'));
expect(uriConnections[1],
Uri(scheme:'ws', host:'[::1]', port:1, path:'/ws'));
expect(uriConnections[2],
Uri(scheme:'ws', host:'[::1]', port:2, path:'/ws'));
final List<FlutterView> views = await connection.getFlutterViews();
expect(views, isNot(null));
expect(views.length, 3);
// Since name can be null, check for the ID on all of them.
expect(views[0].id, 'flutterView0');
expect(views[1].id, 'flutterView1');
expect(views[2].id, 'flutterView2');
expect(views[0].name, equals(null));
expect(views[1].name, 'file://flutterBinary1');
expect(views[2].name, 'file://flutterBinary2');
// Ensure the ports are all closed after stop was called.
await connection.stop();
verify(forwardedPorts[0].stop());
verify(forwardedPorts[1].stop());
verify(forwardedPorts[2].stop());
});
test('end-to-end with three vms and remote open port', () async {
int port = 0;
Future<PortForwarder> mockPortForwardingFunction(
String address,
int remotePort, [
String interface = '',
String configFile,
]) {
return Future<PortForwarder>(() {
final MockPortForwarder pf = MockPortForwarder();
forwardedPorts.add(pf);
when(pf.port).thenReturn(port++);
when(pf.remotePort).thenReturn(remotePort);
when(pf.openPortAddress).thenReturn('fe80::1:2%eno2');
return pf;
});
}
fuchsiaPortForwardingFunction = mockPortForwardingFunction;
final FuchsiaRemoteConnection connection = final FuchsiaRemoteConnection connection =
await FuchsiaRemoteConnection.connectWithSshCommandRunner(mockRunner); await FuchsiaRemoteConnection.connectWithSshCommandRunner(mockRunner);
...@@ -135,6 +198,15 @@ void main() { ...@@ -135,6 +198,15 @@ void main() {
expect(forwardedPorts[1].port, 1); expect(forwardedPorts[1].port, 1);
expect(forwardedPorts[2].port, 2); expect(forwardedPorts[2].port, 2);
// VMs should be accessed via the alternate adddress given by
// [mockPortForwardingFunction].
expect(uriConnections[0],
Uri(scheme:'ws', host:'[fe80::1:2%25eno2]', port:0, path:'/ws'));
expect(uriConnections[1],
Uri(scheme:'ws', host:'[fe80::1:2%25eno2]', port:1, path:'/ws'));
expect(uriConnections[2],
Uri(scheme:'ws', host:'[fe80::1:2%25eno2]', port:2, path:'/ws'));
final List<FlutterView> views = await connection.getFlutterViews(); final List<FlutterView> views = await connection.getFlutterViews();
expect(views, isNot(null)); expect(views, isNot(null));
expect(views.length, 3); expect(views.length, 3);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment