Add FBPortForwarding source code

Summary:
See FBPortForwarding/README.md for an explanation of what this is.
It's required for the upcoming support for physical iOS devices.

To simplify development of the JS app, We're going to bundle the pre-built PortForwardingMacApp
inside the repo, and inside the electron app (static/PortForwardingMacApp.app).
Adding this source so users can build it from source if they choose to.

Reviewed By: danielbuechele

Differential Revision: D13276022

fbshipit-source-id: 99b18e0412cf443bb4a67eb4846cc780e0014de1
This commit is contained in:
John Knox
2018-12-03 11:32:37 -08:00
committed by Facebook Github Bot
parent 98110bc230
commit 8d93946739
11 changed files with 515 additions and 96 deletions

View File

@@ -0,0 +1,17 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the LICENSE
* file in the root directory of this source tree.
*/
#import <Foundation/Foundation.h>
@interface FKPortForwardingClient : NSObject
- (instancetype)init;
- (void)forwardConnectionsToPort:(NSUInteger)port;
- (void)connectToMultiplexingChannelOnPort:(NSUInteger)port;
- (void)close;
@end

View File

@@ -0,0 +1,297 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#import "FKPortForwardingClient.h"
#import <CocoaAsyncSocket/GCDAsyncSocket.h>
#import <peertalk/PTChannel.h>
#import <peertalk/PTUSBHub.h>
#import "FKPortForwardingCommon.h"
static const NSTimeInterval ReconnectDelay = 1.0;
@interface FKPortForwardingClient () <GCDAsyncSocketDelegate, PTChannelDelegate>
{
NSUInteger _destPort;
NSUInteger _channelPort;
NSNumber *_connectingToDeviceID;
NSNumber *_connectedDeviceID;
NSDictionary *_connectedDeviceProperties;
BOOL _notConnectedQueueSuspended;
PTChannel *_connectedChannel;
dispatch_queue_t _notConnectedQueue;
dispatch_queue_t _clientSocketsQueue;
NSMutableDictionary *_clientSockets;
}
@property (atomic, readonly) NSNumber *connectedDeviceID;
@property (atomic, assign) PTChannel *connectedChannel;
@end
@implementation FKPortForwardingClient
@synthesize connectedDeviceID = _connectedDeviceID;
- (instancetype)init
{
if (self = [super init]) {
_notConnectedQueue = dispatch_queue_create("FKPortForwarding.notConnectedQueue", DISPATCH_QUEUE_SERIAL);
_clientSocketsQueue = dispatch_queue_create("FKPortForwarding.clients", DISPATCH_QUEUE_SERIAL);
_clientSockets = [NSMutableDictionary dictionary];
}
return self;
}
- (void)forwardConnectionsToPort:(NSUInteger)port
{
_destPort = port;
}
- (void)connectToMultiplexingChannelOnPort:(NSUInteger)port
{
_channelPort = port;
[self startListeningForDevices];
[self enqueueConnectToLocalIPv4Port];
}
- (void)close
{
[self.connectedChannel close];
}
- (PTChannel *)connectedChannel {
return _connectedChannel;
}
- (void)setConnectedChannel:(PTChannel *)connectedChannel {
_connectedChannel = connectedChannel;
if (!_connectedChannel) {
for (GCDAsyncSocket *sock in [_clientSockets objectEnumerator]) {
[sock setDelegate:nil];
[sock disconnect];
}
[_clientSockets removeAllObjects];
}
// Toggle the notConnectedQueue_ depending on if we are connected or not
if (!_connectedChannel && _notConnectedQueueSuspended) {
dispatch_resume(_notConnectedQueue);
_notConnectedQueueSuspended = NO;
} else if (_connectedChannel && !_notConnectedQueueSuspended) {
dispatch_suspend(_notConnectedQueue);
_notConnectedQueueSuspended = YES;
}
if (!_connectedChannel && _connectingToDeviceID) {
[self enqueueConnectToUSBDevice];
}
}
#pragma mark - PTChannelDelegate
- (void)ioFrameChannel:(PTChannel *)channel didReceiveFrameOfType:(uint32_t)type tag:(uint32_t)tag payload:(PTData *)payload {
//NSLog(@"received %@, %u, %u, %@", channel, type, tag, payload);
if (type == FKPortForwardingFrameTypeOpenPipe) {
GCDAsyncSocket *sock = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:_clientSocketsQueue];
sock.userData = @(tag);
_clientSockets[@(tag)] = sock;
NSError *connectError;
if (![sock connectToHost:@"localhost" onPort:_destPort error:&connectError]) {
FBPFLog(@"Failed to connect to local %lu - %@", (unsigned long)_destPort, connectError);
}
FBPFTrace(@"open socket (%d)", tag);
}
if (type == FKPortForwardingFrameTypeWriteToPipe) {
GCDAsyncSocket *sock = _clientSockets[@(tag)];
[sock writeData:[NSData dataWithBytes:payload.data length:payload.length] withTimeout:-1 tag:0];
FBPFTrace(@"channel -> socket (%d) %zu bytes", tag, payload.length);
}
if (type == FKPortForwardingFrameTypeClosePipe) {
GCDAsyncSocket *sock = _clientSockets[@(tag)];
[sock disconnectAfterWriting];
FBPFTrace(@"close socket (%d)", tag);
}
}
- (void)ioFrameChannel:(PTChannel *)channel didEndWithError:(NSError *)error {
if (_connectedDeviceID && [_connectedDeviceID isEqualToNumber:channel.userInfo]) {
[self didDisconnectFromDevice:_connectedDeviceID];
}
if (_connectedChannel == channel) {
FBPFTrace(@"Disconnected from %@", channel.userInfo);
self.connectedChannel = nil;
}
}
#pragma mark - GCDAsyncSocketDelegate
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port
{
FBPFTrace(@"socket (%ld) connected to %@", (long)[sock.userData integerValue], host);
[sock readDataWithTimeout:-1 tag:0];
}
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err
{
UInt32 tag = [sock.userData unsignedIntValue];
[_clientSockets removeObjectForKey:@(tag)];
FBPFTrace(@"socket (%d) disconnected", (unsigned int)tag);
[_connectedChannel sendFrameOfType:FKPortForwardingFrameTypeClosePipe tag:tag withPayload:nil callback:nil];
}
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)_
{
UInt32 tag = [sock.userData unsignedIntValue];
[_connectedChannel sendFrameOfType:FKPortForwardingFrameTypeWriteToPipe tag:tag withPayload:NSDataToGCDData(data) callback:^(NSError *error) {
FBPFTrace(@"channel -> socket (%d), %lu bytes", (unsigned int)tag, (unsigned long)data.length);
[sock readDataWithTimeout:-1 tag:0];
}];
}
#pragma mark - Wired device connections
- (void)startListeningForDevices {
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
__weak typeof(self) weakSelf = self;
[nc addObserverForName:PTUSBDeviceDidAttachNotification object:PTUSBHub.sharedHub queue:nil usingBlock:^(NSNotification *note) {
NSNumber *deviceID = [note.userInfo objectForKey:@"DeviceID"];
//NSLog(@"PTUSBDeviceDidAttachNotification: %@", note.userInfo);
FBPFTrace(@"PTUSBDeviceDidAttachNotification: %@", deviceID);
typeof(self) strongSelf = weakSelf;
if (!strongSelf) {
return;
}
dispatch_async(strongSelf->_notConnectedQueue, ^{
[strongSelf didAttachToDevice:deviceID note:note];
});
}];
[nc addObserverForName:PTUSBDeviceDidDetachNotification object:PTUSBHub.sharedHub queue:nil usingBlock:^(NSNotification *note) {
NSNumber *deviceID = [note.userInfo objectForKey:@"DeviceID"];
//NSLog(@"PTUSBDeviceDidDetachNotification: %@", note.userInfo);
FBPFTrace(@"PTUSBDeviceDidDetachNotification: %@", deviceID);
[weakSelf didDetachFromDevice:deviceID];
}];
}
- (void)didAttachToDevice:(NSNumber *)deviceID note:(NSNotification *)note
{
if (!_connectingToDeviceID || ![deviceID isEqualToNumber:_connectingToDeviceID]) {
[self disconnectFromCurrentChannel];
_connectingToDeviceID = deviceID;
_connectedDeviceProperties = [note.userInfo objectForKey:@"Properties"];
[self enqueueConnectToUSBDevice];
}
}
- (void)didDetachFromDevice:(NSNumber *)deviceID
{
if ([_connectingToDeviceID isEqualToNumber:deviceID]) {
_connectedDeviceProperties = nil;
_connectingToDeviceID = nil;
if (_connectedChannel) {
[_connectedChannel close];
}
}
}
- (void)didDisconnectFromDevice:(NSNumber *)deviceID {
FBPFLog(@"Disconnected from device #%@", deviceID);
if ([_connectedDeviceID isEqualToNumber:deviceID]) {
[self willChangeValueForKey:@"connectedDeviceID"];
_connectedDeviceID = nil;
[self didChangeValueForKey:@"connectedDeviceID"];
}
}
- (void)disconnectFromCurrentChannel {
if (_connectedDeviceID && _connectedChannel) {
[_connectedChannel close];
self.connectedChannel = nil;
}
}
- (void)enqueueConnectToLocalIPv4Port {
dispatch_async(_notConnectedQueue, ^{
dispatch_async(dispatch_get_main_queue(), ^{
[self connectToLocalIPv4Port];
});
});
}
- (void)connectToLocalIPv4Port {
PTChannel *channel = [PTChannel channelWithDelegate:self];
channel.userInfo = [NSString stringWithFormat:@"127.0.0.1:%lu", (unsigned long)_channelPort];
[channel connectToPort:_channelPort IPv4Address:INADDR_LOOPBACK callback:^(NSError *error, PTAddress *address) {
if (error) {
if (error.domain == NSPOSIXErrorDomain && (error.code == ECONNREFUSED || error.code == ETIMEDOUT)) {
// this is an expected state
} else {
FBPFTrace(@"Failed to connect to 127.0.0.1:%lu: %@", (unsigned long)_channelPort, error);
}
} else {
[self disconnectFromCurrentChannel];
self.connectedChannel = channel;
channel.userInfo = address;
FBPFLog(@"Connected to %@", address);
}
[self performSelector:@selector(enqueueConnectToLocalIPv4Port) withObject:nil afterDelay:ReconnectDelay];
}];
}
- (void)enqueueConnectToUSBDevice {
dispatch_async(_notConnectedQueue, ^{
dispatch_async(dispatch_get_main_queue(), ^{
[self connectToUSBDevice];
});
});
}
- (void)connectToUSBDevice {
PTChannel *channel = [PTChannel channelWithDelegate:self];
channel.userInfo = _connectingToDeviceID;
channel.delegate = self;
[channel connectToPort:(int)_channelPort overUSBHub:PTUSBHub.sharedHub deviceID:_connectingToDeviceID callback:^(NSError *error) {
[self didConnectToChannel:channel withError:error];
}];
}
- (void)didConnectToChannel:(PTChannel *)channel withError:(NSError *)error
{
if (error) {
FBPFTrace(@"Failed to connect to device #%@: %@", channel.userInfo, error);
if (channel.userInfo == _connectingToDeviceID) {
[self performSelector:@selector(enqueueConnectToUSBDevice) withObject:nil afterDelay:ReconnectDelay];
}
} else {
_connectedDeviceID = _connectingToDeviceID;
self.connectedChannel = channel;
FBPFLog(@"Connected to device #%@\n%@", _connectingToDeviceID, _connectedDeviceProperties);
}
}
@end

View File

@@ -0,0 +1,23 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the LICENSE
* file in the root directory of this source tree.
*/
#import <Foundation/Foundation.h>
#define FBPFTrace(...) /*NSLog(__VA_ARGS__)*/
#define FBPFLog(...) NSLog(__VA_ARGS__)
enum {
FKPortForwardingFrameTypeOpenPipe = 201,
FKPortForwardingFrameTypeWriteToPipe = 202,
FKPortForwardingFrameTypeClosePipe = 203,
};
static dispatch_data_t NSDataToGCDData(NSData *data) {
__block NSData *retainedData = data;
return dispatch_data_create(data.bytes, data.length, nil, ^{
retainedData = nil;
});
}

View File

@@ -0,0 +1,17 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the LICENSE
* file in the root directory of this source tree.
*/
#import <Foundation/Foundation.h>
@interface FKPortForwardingServer : NSObject
- (instancetype)init;
- (void)listenForMultiplexingChannelOnPort:(NSUInteger)port;
- (void)forwardConnectionsFromPort:(NSUInteger)port;
- (void)close;
@end

View File

@@ -0,0 +1,185 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#import "FKPortForwardingServer.h"
#import <UIKit/UIKit.h>
#import <CocoaAsyncSocket/GCDAsyncSocket.h>
#import <peertalk/PTChannel.h>
#import "FKPortForwardingCommon.h"
@interface FKPortForwardingServer () <PTChannelDelegate, GCDAsyncSocketDelegate>
{
__weak PTChannel *_serverChannel;
__weak PTChannel *_peerChannel;
GCDAsyncSocket *_serverSocket;
NSMutableDictionary *_clientSockets;
UInt32 _lastClientSocketTag;
dispatch_queue_t _socketQueue;
PTProtocol *_protocol;
}
@end
@implementation FKPortForwardingServer
- (instancetype)init
{
if (self = [super init]) {
_socketQueue = dispatch_queue_create("FKPortForwardingServer", DISPATCH_QUEUE_SERIAL);
_lastClientSocketTag = 0;
_clientSockets = [NSMutableDictionary dictionary];
_protocol = [[PTProtocol alloc] initWithDispatchQueue:_socketQueue];
}
return self;
}
- (void)dealloc
{
[self close];
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)forwardConnectionsFromPort:(NSUInteger)port
{
[self _forwardConnectionsFromPort:port reportError:YES];
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidBecomeActiveNotification object:nil queue:nil usingBlock:^(NSNotification *note) {
[self _forwardConnectionsFromPort:port reportError:NO];
}];
}
- (void)_forwardConnectionsFromPort:(NSUInteger)port reportError:(BOOL)shouldReportError
{
GCDAsyncSocket *serverSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:_socketQueue];
NSError *listenError;
if ([serverSocket acceptOnPort:port error:&listenError]) {
_serverSocket = serverSocket;
} else {
if (shouldReportError) {
FBPFLog(@"Failed to listen: %@", listenError);
}
}
}
- (void)listenForMultiplexingChannelOnPort:(NSUInteger)port
{
[self _listenForMultiplexingChannelOnPort:port reportError:YES];
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidBecomeActiveNotification object:nil queue:nil usingBlock:^(NSNotification *note) {
[self _listenForMultiplexingChannelOnPort:port reportError:NO];
}];
}
- (void)_listenForMultiplexingChannelOnPort:(NSUInteger)port reportError:(BOOL)shouldReportError
{
PTChannel *channel = [[PTChannel alloc] initWithProtocol:_protocol delegate:self];
[channel listenOnPort:port IPv4Address:INADDR_LOOPBACK callback:^(NSError *error) {
if (error) {
if (shouldReportError) {
FBPFLog(@"Failed to listen on 127.0.0.1:%lu: %@", (unsigned long)port, error);
}
} else {
FBPFTrace(@"Listening on 127.0.0.1:%lu", (unsigned long)port);
self->_serverChannel = channel;
}
}];
}
- (void)close
{
if (_serverChannel) {
[_serverChannel close];
_serverChannel = nil;
}
[_serverSocket disconnect];
}
#pragma mark - PTChannelDelegate
- (void)ioFrameChannel:(PTChannel *)channel didAcceptConnection:(PTChannel *)otherChannel fromAddress:(PTAddress *)address {
// Cancel any other connection. We are FIFO, so the last connection
// established will cancel any previous connection and "take its place".
if (_peerChannel) {
[_peerChannel cancel];
}
// Weak pointer to current connection. Connection objects live by themselves
// (owned by its parent dispatch queue) until they are closed.
_peerChannel = otherChannel;
_peerChannel.userInfo = address;
FBPFTrace(@"Connected to %@", address);
}
- (void)ioFrameChannel:(PTChannel *)channel didReceiveFrameOfType:(uint32_t)type tag:(uint32_t)tag payload:(PTData *)payload {
//NSLog(@"didReceiveFrameOfType: %u, %u, %@", type, tag, payload);
if (type == FKPortForwardingFrameTypeWriteToPipe) {
GCDAsyncSocket *sock = _clientSockets[@(tag)];
[sock writeData:[NSData dataWithBytes:payload.data length:payload.length] withTimeout:-1 tag:0];
FBPFTrace(@"channel -> socket (%d), %zu bytes", tag, payload.length);
}
if (type == FKPortForwardingFrameTypeClosePipe) {
GCDAsyncSocket *sock = _clientSockets[@(tag)];
[sock disconnectAfterWriting];
}
}
- (void)ioFrameChannel:(PTChannel *)channel didEndWithError:(NSError *)error {
for (GCDAsyncSocket *sock in [_clientSockets objectEnumerator]) {
[sock setDelegate:nil];
[sock disconnect];
}
[_clientSockets removeAllObjects];
FBPFTrace(@"Disconnected from %@, error = %@", channel.userInfo, error);
}
#pragma mark - GCDAsyncSocketDelegate
- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket
{
dispatch_block_t block = ^() {
if (!self->_peerChannel) {
[newSocket setDelegate:nil];
[newSocket disconnect];
}
UInt32 tag = ++self->_lastClientSocketTag;
newSocket.userData = @(tag);
newSocket.delegate = self;
self->_clientSockets[@(tag)] = newSocket;
[self->_peerChannel sendFrameOfType:FKPortForwardingFrameTypeOpenPipe tag:self->_lastClientSocketTag withPayload:nil callback:^(NSError *error) {
FBPFTrace(@"open socket (%d), error = %@", (unsigned int)tag, error);
[newSocket readDataWithTimeout:-1 tag:0];
}];
};
if (_peerChannel) {
block();
} else {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), _socketQueue, block);
}
}
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)_
{
UInt32 tag = [[sock userData] unsignedIntValue];
FBPFTrace(@"Incoming data on socket (%d) - %lu bytes", (unsigned int)tag, (unsigned long)data.length);
[_peerChannel sendFrameOfType:FKPortForwardingFrameTypeWriteToPipe tag:tag withPayload:NSDataToGCDData(data) callback:^(NSError *error) {
FBPFTrace(@"socket (%d) -> channel %lu bytes, error = %@", (unsigned int)tag, (unsigned long)data.length, error);
[sock readDataWithTimeout:-1 tag:_];
}];
}
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err
{
UInt32 tag = [sock.userData unsignedIntValue];
[_clientSockets removeObjectForKey:@(tag)];
[_peerChannel sendFrameOfType:FKPortForwardingFrameTypeClosePipe tag:tag withPayload:nil callback:^(NSError *error) {
FBPFTrace(@"socket (%d) disconnected, err = %@, peer error = %@", (unsigned int)tag, err, error);
}];
}
@end

View File

@@ -0,0 +1,66 @@
# FKPortForwarding
FKPortForwarding lets you expose your Mac's port to iOS device via lightning
cable. The typical usecase is connecting to a TCP server that runs on OS X
from an iPhone app without common WiFi network.
## Benefits:
1. No need to be on the same WiFi, worry about firewalls (fbguest) or VPN
2. iOS app doesn't have to know your Mac's IP address
3. Secure - communication is possible only when connected via USB
## How it works
iOS provides a way to connect to device's TCP server from Mac via USBHub, but
there is no API to connect from iOS to TCP server running on Mac. FKPortForwarding
uses [Peertalk](https://github.com/rsms/peertalk) to establish communication
channel from Mac to iOS, creates a TCP server on iOS and multiplexes all
connections to that server via the peertalk channel. Helper app running on Mac
listens for commands on the peertalk channel and initializes TCP connections
to local port and forwards all communication back via the same peertalk channel.
|
iOS Device | Mac
|
+----------------+ +----------------+
|Peertalk Server | connect |Peertalk Client |
| <------------+ |
| | | |
| Port 8025| | |
+----+-----------+ +---------^------+
| |
| |
incoming +----------------+ | | +--------------+
connections |Proxy Server | | | |Real Server |
------------->> | | +-------------+ commands | | |
| Port 8081| | create | | stream | | Port 8081|
+-+--------------+ +---------> Peertalk <----------+ +-^------------+
| | Channel | ^
| +--------+ | | +--------+ | outgoing
| | | onConnect | | connect | | | connections
+---> Client +---------------> OpenPipe +---------------> Client +-----+
| #[tag] | onRead | | write | #[tag] |
| +---------------> WriteToPipe +---------------> |
| | onDisconnect | | disconnect | |
| +---------------> ClosePipe +---------------> |
| | | | | |
| | write | | onRead | |
| <---------------+ WriteToPipe <---------------+ |
| | close | | onDisconnect | |
| <---------------+ ClosePipe <---------------+ |
| | | | | |
+--------+ | | +--------+
+-------------+
First, the library on iOS device creates a TCP server on the port we want to
forward (let's say 8081) and a special Peertalk server on port 8025. Mac helper
app looks for connected iOS devices, and once it finds one it connects to its
peertalk server. Only *one* channel is created that's going to be used for
multiplexing data.
When a socket connects to local proxy server, FKPortForwarding is going to assign
a tag to the connection and use peertalk channel to tell Mac helper app to connect
to TCP port 8081 on Mac. Now events and data on both sides of the wire are going
to be multiplexed and transferred via the peertalk channel.