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 {
@override
int get remotePort => _remotePort;
@override
String get openPortAddress => InternetAddress.loopbackIPv4.address;
@override
Future<void> stop() async { }
}
......
......@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:core';
import 'dart:io';
/// Determines whether `address` is a valid IPv6 or IPv4 address.
///
......@@ -17,7 +17,7 @@ void validateAddress(String address) {
/// Returns true if `address` is a valid IPv6 address.
bool isIpV6Address(String address) {
try {
Uri.parseIPv6Address(address);
InternetAddress(address, type:InternetAddressType.IPv6);
return true;
} on FormatException {
return false;
......@@ -27,7 +27,7 @@ bool isIpV6Address(String address) {
/// Returns true if `address` is a valid IPv4 address.
bool isIpV4Address(String address) {
try {
Uri.parseIPv4Address(address);
InternetAddress(address, type:InternetAddressType.IPv4);
return true;
} on FormatException {
return false;
......
......@@ -103,13 +103,13 @@ class DartVmEvent {
/// This class can be connected to several instances of the Fuchsia device's
/// Dart VM at any given time.
class FuchsiaRemoteConnection {
FuchsiaRemoteConnection._(this._useIpV6Loopback, this._sshCommandRunner)
FuchsiaRemoteConnection._(this._useIpV6, this._sshCommandRunner)
: _pollDartVms = false;
bool _pollDartVms;
final List<PortForwarder> _forwardedVmServicePorts = <PortForwarder>[];
final SshCommandRunner _sshCommandRunner;
final bool _useIpV6Loopback;
final bool _useIpV6;
/// A mapping of Dart VM ports (as seen on the target machine), to
/// [PortForwarder] instances mapping from the local machine to the target
......@@ -126,15 +126,15 @@ class FuchsiaRemoteConnection {
StreamController<DartVmEvent>();
/// VM service cache to avoid repeating handshakes across function
/// calls. Keys a forwarded port to a DartVm connection instance.
final Map<int, DartVm> _dartVmCache = <int, DartVm>{};
/// calls. Keys a URI to a DartVm connection instance.
final Map<Uri, DartVm> _dartVmCache = <Uri, DartVm>{};
/// Same as [FuchsiaRemoteConnection.connect] albeit with a provided
/// [SshCommandRunner] instance.
static Future<FuchsiaRemoteConnection> connectWithSshCommandRunner(SshCommandRunner commandRunner) async {
final FuchsiaRemoteConnection connection = FuchsiaRemoteConnection._(
isIpV6Address(commandRunner.address), commandRunner);
await connection._forwardLocalPortsToDeviceServicePorts();
await connection._forwardOpenPortsToDeviceServicePorts();
Stream<DartVmEvent> dartVmStream() {
Future<void> listen() async {
......@@ -224,14 +224,16 @@ class FuchsiaRemoteConnection {
for (final PortForwarder pf in _forwardedVmServicePorts) {
// Closes VM service first to ensure that the connection is closed cleanly
// on the target before shutting down the forwarding itself.
final DartVm vmService = _dartVmCache[pf.port];
_dartVmCache[pf.port] = null;
final Uri uri = _getDartVmUri(pf);
final DartVm vmService = _dartVmCache[uri];
_dartVmCache[uri] = null;
await vmService?.stop();
await pf.stop();
}
for (final PortForwarder pf in _dartVmPortMap.values) {
final DartVm vmService = _dartVmCache[pf.port];
_dartVmCache[pf.port] = null;
final Uri uri = _getDartVmUri(pf);
final DartVm vmService = _dartVmCache[uri];
_dartVmCache[uri] = null;
await vmService?.stop();
await pf.stop();
}
......@@ -258,7 +260,7 @@ class FuchsiaRemoteConnection {
if (event.eventType == DartVmEventType.started) {
_log.fine('New VM found on port: ${event.servicePort}. Searching '
'for Isolate: $pattern');
final DartVm vmService = await _getDartVm(event.uri.port,
final DartVm vmService = await _getDartVm(event.uri,
timeout: _kDartVmConnectionTimeout);
// If the VM service is null, set the result to the empty list.
final List<IsolateRef> result = await vmService
......@@ -307,7 +309,7 @@ class FuchsiaRemoteConnection {
<Future<List<IsolateRef>>>[];
for (final PortForwarder fp in _dartVmPortMap.values) {
final DartVm vmService =
await _getDartVm(fp.port, timeout: vmConnectionTimeout);
await _getDartVm(_getDartVmUri(fp), timeout: vmConnectionTimeout);
if (vmService == null) {
continue;
}
......@@ -385,13 +387,13 @@ class FuchsiaRemoteConnection {
_dartVmEventController.add(DartVmEvent._(
eventType: DartVmEventType.stopped,
servicePort: pf.remotePort,
uri: _getDartVmUri(pf.port),
uri: _getDartVmUri(pf),
));
}
}
for (final PortForwarder pf in _dartVmPortMap.values) {
final DartVm service = await _getDartVm(pf.port);
final DartVm service = await _getDartVm(_getDartVmUri(pf));
if (service == null) {
await shutDownPortForwarder(pf);
} else {
......@@ -402,15 +404,14 @@ class FuchsiaRemoteConnection {
return result;
}
Uri _getDartVmUri(int port) {
// While the IPv4 loopback can be used for the initial port forwarding
// (see [PortForwarder.start]), the address is actually bound to the IPv6
// loopback device, so connecting to the IPv4 loopback would fail when the
// target address is IPv6 link-local.
final String addr = _useIpV6Loopback
? 'http://[$_ipv6Loopback]:$port'
: 'http://$_ipv4Loopback:$port';
final Uri uri = Uri.parse(addr);
Uri _getDartVmUri(PortForwarder pf) {
String addr;
if (pf.openPortAddress == null) {
addr = _useIpV6 ? '[$_ipv6Loopback]' : _ipv4Loopback;
} else {
addr = _useIpV6 ? '[${pf.openPortAddress}]' : pf.openPortAddress;
}
final Uri uri = Uri.http('$addr:${pf.port}', '/');
return uri;
}
......@@ -419,17 +420,16 @@ class FuchsiaRemoteConnection {
/// Returns null if either there is an [HttpException] or a
/// [TimeoutException], else a [DartVm] instance.
Future<DartVm> _getDartVm(
int port, {
Uri uri, {
Duration timeout = _kDartVmConnectionTimeout,
}) async {
if (!_dartVmCache.containsKey(port)) {
if (!_dartVmCache.containsKey(uri)) {
// 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 instance is shut down in the middle of communicating.
try {
final DartVm dartVm =
await DartVm.connect(_getDartVmUri(port), timeout: timeout);
_dartVmCache[port] = dartVm;
final DartVm dartVm = await DartVm.connect(uri, timeout: timeout);
_dartVmCache[uri] = dartVm;
} on HttpException {
_log.warning('HTTP Exception encountered connecting to new VM');
return null;
......@@ -438,7 +438,7 @@ class FuchsiaRemoteConnection {
return null;
}
}
return _dartVmCache[port];
return _dartVmCache[uri];
}
/// Checks for changes in the list of Dart VM instances.
......@@ -460,7 +460,7 @@ class FuchsiaRemoteConnection {
_dartVmEventController.add(DartVmEvent._(
eventType: DartVmEventType.started,
servicePort: servicePort,
uri: _getDartVmUri(_dartVmPortMap[servicePort].port),
uri: _getDartVmUri(_dartVmPortMap[servicePort]),
));
}
}
......@@ -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
/// are reset by way of [stop].
Future<void> _forwardLocalPortsToDeviceServicePorts() async {
Future<void> _forwardOpenPortsToDeviceServicePorts() async {
await stop();
final List<int> servicePorts = await getDeviceServicePorts();
final List<PortForwarder> forwardedVmServicePorts =
......@@ -548,9 +548,13 @@ class FuchsiaRemoteConnection {
///
/// To shut down a port forwarder you must call the [stop] function.
abstract class PortForwarder {
/// Determines the port which is being forwarded from the local machine.
/// Determines the port which is being forwarded.
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.
int get remotePort;
......@@ -581,6 +585,9 @@ class _SshPortForwarder implements PortForwarder {
@override
int get port => _localSocket.port;
@override
String get openPortAddress => _ipV6 ? _ipv6Loopback : _ipv4Loopback;
@override
int get remotePort => _remotePort;
......@@ -602,8 +609,9 @@ class _SshPortForwarder implements PortForwarder {
// TODO(awdavies): The square-bracket enclosure for using the IPv6 loopback
// 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
// loopback (::1). While this can be used for forwarding to the destination
// IPv6 interface, it cannot be used to connect to a websocket.
// loopback (::1). Therefore, while the IPv4 loopback can be used for
// forwarding to the destination IPv6 interface, when connecting to the
// websocket, the IPV6 loopback should be used.
final String formattedForwardingUrl =
'${localSocket.port}:$_ipv4Loopback:$remotePort';
final String targetAddress =
......
......@@ -31,23 +31,6 @@ void main() {
const String interface = 'eno1';
when(mockRunner.address).thenReturn(address);
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 =
<Map<String, dynamic>>[
<String, dynamic>{
......@@ -90,6 +73,7 @@ void main() {
},
];
forwardedPorts = <MockPortForwarder>[];
mockPeerConnections = <MockPeer>[];
uriConnections = <Uri>[];
Future<json_rpc.Peer> mockVmConnectionFunction(
......@@ -109,7 +93,6 @@ void main() {
});
}
fuchsiaPortForwardingFunction = mockPortForwardingFunction;
fuchsiaVmServiceConnectionFunction = mockVmConnectionFunction;
});
......@@ -121,6 +104,86 @@ void main() {
});
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 =
await FuchsiaRemoteConnection.connectWithSshCommandRunner(mockRunner);
......@@ -135,6 +198,15 @@ void main() {
expect(forwardedPorts[1].port, 1);
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();
expect(views, isNot(null));
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