Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Implement no-encrypt-traffic in npt #1401

Merged
merged 11 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
262 changes: 189 additions & 73 deletions packages/dart/noports_core/lib/src/srv/srv_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -407,11 +407,15 @@ class SrvImplDart implements Srv<SocketConnector> {
Future<SocketConnector> run() async {
try {
var hosts = await InternetAddress.lookup(streamingHost);

late SocketConnector sc;
// Determines whether the traffic in the socket is encrypted or transmitted in plain text.
bool encryptRvdTraffic =
(sessionAESKeyString != null && sessionIVString != null);

if (bindLocalPort) {
if (multi) {
if (sessionAESKeyString == null || sessionIVString == null) {
if (encryptRvdTraffic == true &&
(sessionAESKeyString == null || sessionIVString == null)) {
throw ArgumentError('Symmetric session encryption key required');
}
sc = await _runClientSideMulti(hosts: hosts, timeout: timeout);
Expand All @@ -421,7 +425,8 @@ class SrvImplDart implements Srv<SocketConnector> {
} else {
// daemon side
if (multi) {
if (sessionAESKeyString == null || sessionIVString == null) {
if (encryptRvdTraffic == true &&
(sessionAESKeyString == null || sessionIVString == null)) {
throw ArgumentError('Symmetric session encryption key required');
}
sc = await _runDaemonSideMulti(hosts: hosts, timeout: timeout);
Expand Down Expand Up @@ -485,7 +490,6 @@ class SrvImplDart implements Srv<SocketConnector> {
}
},
);

return sc;
}

Expand All @@ -495,7 +499,6 @@ class SrvImplDart implements Srv<SocketConnector> {
}) async {
// client side
SocketConnector? socketConnector;

Socket sessionControlSocket = await Socket.connect(
streamingHost, streamingPort,
timeout: Duration(seconds: 10));
Expand All @@ -505,6 +508,74 @@ class SrvImplDart implements Srv<SocketConnector> {
' control socket connection to rvd');
sessionControlSocket.writeln(rvdAuthString);
}

if (sessionAESKeyString != null && sessionIVString != null) {
logger
.info('_runClientSideMulti: On the client-side traffic is encrypted');
socketConnector = await _clientSideEncryptedSocket(
sessionControlSocket, socketConnector, hosts, timeout);
} else {
logger.info(
'_runClientSideMulti: On the client-side traffic is transmitted in plain text');
socketConnector = await _clientSidePlainSocket(
sessionControlSocket, socketConnector, hosts, timeout);
}

logger.info('_runClientSideMulti serverToSocket is ready');
// upon socketConnector.done, destroy the control socket, and complete
unawaited(socketConnector.done.whenComplete(() {
logger.info('_runClientSideMulti sc.done');
sessionControlSocket.destroy();
}));
return socketConnector;
}

/// On the client side, the data in this socket remains unencrypted and is transmitted in plain text
Future<SocketConnector> _clientSidePlainSocket(
Socket sessionControlSocket,
SocketConnector? socketConnector,
List<InternetAddress> hosts,
Duration timeout) async {
sessionControlSocket.listen((event) {
String response = String.fromCharCodes(event).trim();
logger.info('_runClientSideMulti'
' Received control socket response: [$response]');
}, onError: (e) {
logger.severe('_runClientSideMulti controlSocket error: $e');
socketConnector?.close();
}, onDone: () {
logger.info('_runClientSideMulti controlSocket done');
socketConnector?.close();
});
socketConnector = await SocketConnector.serverToSocket(
portA: localPort,
addressB: hosts[0],
portB: streamingPort,
verbose: false,
logger: ioSinkForLogger(logger),
multi: multi,
timeout: timeout,
beforeJoining: (Side sideA, Side sideB) {
logger.info('_runClientSideMulti Sending connect request');
sessionControlSocket
.add(Uint8List.fromList('connect:no:encrypt\n'.codeUnits));
// Authenticate the sideB socket (to the rvd)
if (rvdAuthString != null) {
logger
.info('_runClientSideMulti authenticating new connection to rvd');
sideB.socket.writeln(rvdAuthString);
}
},
);
return socketConnector;
}

/// On the client side, the data in encrypted and is transmitted through this socket.
Future<SocketConnector> _clientSideEncryptedSocket(
Socket sessionControlSocket,
SocketConnector? socketConnector,
List<InternetAddress> hosts,
Duration timeout) async {
DataTransformer controlEncrypter =
createEncrypter(sessionAESKeyString!, sessionIVString!);
DataTransformer controlDecrypter =
Expand Down Expand Up @@ -556,14 +627,6 @@ class SrvImplDart implements Srv<SocketConnector> {
sideB.transformer = createDecrypter(socketAESKey, socketIV);
},
);
logger.info('_runClientSideMulti serverToSocket is ready');

// upon socketConnector.done, destroy the control socket, and complete
unawaited(socketConnector.done.whenComplete(() {
logger.info('_runClientSideMulti sc.done');
sessionControlSocket.destroy();
}));

return socketConnector;
}

Expand All @@ -575,30 +638,50 @@ class SrvImplDart implements Srv<SocketConnector> {
List<String> args = request.split(":");
switch (args.first) {
case 'connect':
if (args.length != 3) {
logger.severe('Unknown request to control socket: [$request]');
// Handles the request from the socket where data needs no encryption.
// When --no-encrypt-rvd-traffic flag is set to true.
if (request == 'connect:no:encrypt') {
await SocketConnector.socketToSocket(
connector: sc,
addressA:
(await InternetAddress.lookup(localHost ?? 'localhost'))[0],
portA: localPort,
addressB: hosts[0],
portB: streamingPort,
verbose: false,
logger: ioSinkForLogger(logger));
if (rvdAuthString != null) {
logger.info('_runDaemonSideMulti authenticating'
' new socket connection to rvd');
sc.connections.last.sideB.socket.writeln(rvdAuthString);
}
return;
} else {
// In this case, the data in the socket is encrypted.
if (args.length != 3) {
logger.severe('Unknown request to control socket: [$request]');
return;
}
logger.info('_runDaemonSideMulti'
' Control socket received ${args.first} request - '
' creating new socketToSocket connection');
await SocketConnector.socketToSocket(
connector: sc,
addressA:
(await InternetAddress.lookup(localHost ?? 'localhost'))[0],
portA: localPort,
addressB: hosts[0],
portB: streamingPort,
verbose: false,
logger: ioSinkForLogger(logger),
transformAtoB: createEncrypter(args[1], args[2]),
transformBtoA: createDecrypter(args[1], args[2]));
if (rvdAuthString != null) {
logger.info('_runDaemonSideMulti authenticating'
' new socket connection to rvd');
sc.connections.last.sideB.socket.writeln(rvdAuthString);
}
}
logger.info('_runDaemonSideMulti'
' Control socket received ${args.first} request - '
' creating new socketToSocket connection');
await SocketConnector.socketToSocket(
connector: sc,
addressA:
(await InternetAddress.lookup(localHost ?? 'localhost'))[0],
portA: localPort,
addressB: hosts[0],
portB: streamingPort,
verbose: false,
logger: ioSinkForLogger(logger),
transformAtoB: createEncrypter(args[1], args[2]),
transformBtoA: createDecrypter(args[1], args[2]));
if (rvdAuthString != null) {
logger.info('_runDaemonSideMulti authenticating'
' new socket connection to rvd');
sc.connections.last.sideB.socket.writeln(rvdAuthString);
}

break;
default:
logger.severe('Unknown request to control socket: [$request]');
Expand All @@ -622,6 +705,41 @@ class SrvImplDart implements Srv<SocketConnector> {
' control socket connection to rvd');
sessionControlSocket.writeln(rvdAuthString);
}

if (sessionAESKeyString != null && sessionIVString != null) {
logger
.info('_runDaemonSideMulti: On the daemon side traffic is encrypted');
_daemonSideEncryptedSocket(sessionControlSocket, sc, hosts);
} else {
logger.info(
'_runDaemonSideMulti: On the daemon side traffic is transmitted in plain text');
_daemonSidePlainSocket(sessionControlSocket, sc, hosts);
}

// upon socketConnector.done, destroy the control socket, and complete
unawaited(sc.done.whenComplete(() {
sessionControlSocket.destroy();
}));

return sc;
}

void _daemonSidePlainSocket(Socket sessionControlSocket, SocketConnector sc,
List<InternetAddress> hosts) {
Mutex controlStreamMutex = Mutex();
sessionControlSocket.listen((event) async {
await _sessionControlSocketListener(controlStreamMutex, event, sc, hosts);
}, onError: (e) {
logger.severe('controlSocket error: $e');
sc.close();
}, onDone: () {
logger.info('controlSocket done');
sc.close();
});
}

void _daemonSideEncryptedSocket(Socket sessionControlSocket,
SocketConnector sc, List<InternetAddress> hosts) {
DataTransformer controlEncrypter =
createEncrypter(sessionAESKeyString!, sessionIVString!);
DataTransformer controlDecrypter =
Expand All @@ -636,52 +754,50 @@ class SrvImplDart implements Srv<SocketConnector> {
Mutex controlStreamMutex = Mutex();
controlStream.listen((event) async {
logger.info('Received event on control socket.');
try {
await controlStreamMutex.acquire();
if (event.isEmpty) {
logger.info('Empty control message (Uint8List) received');
return;
}
String eventStr = String.fromCharCodes(event).trim();
if (eventStr.isEmpty) {
logger.info('Empty control message (String) received');
return;
}
// TODO The code below (splitting by `connect:`) resolves a
// particular issue for the moment, but the overall approach
// to handling control messages needs to be redone, e.g. :
// Ideally - send the control request, and a newline
// => as of this commit, this is the case
// Receive - wait for newline, handle the request, repeat
// => older npt clients don't send `\n` so we will need to add some
// magic to handle both (a) older clients which don't send `\n`
// as well as (b) newer ones which do. Cleanest is to add a
// flag to the npt request from the client stating that it sends
// `\n` . If so then we handle that cleanly; if not then we use
// this approach (split by `connect:`)
List<String> requests = eventStr.split('connect:');
for (String request in requests) {
if (request.isNotEmpty) {
await _handleMultiConnectRequest(sc, hosts, 'connect:$request');
}
}
} finally {
controlStreamMutex.release();
}
await _sessionControlSocketListener(controlStreamMutex, event, sc, hosts);
}, onError: (e) {
logger.severe('controlSocket error: $e');
sc.close();
}, onDone: () {
logger.info('controlSocket done');
sc.close();
});
}

// upon socketConnector.done, destroy the control socket, and complete
unawaited(sc.done.whenComplete(() {
sessionControlSocket.destroy();
}));

return sc;
Future<void> _sessionControlSocketListener(Mutex controlStreamMutex,
List<int> event, SocketConnector sc, List<InternetAddress> hosts) async {
try {
await controlStreamMutex.acquire();
if (event.isEmpty) {
logger.info('Empty control message (Uint8List) received');
return;
}
String eventStr = String.fromCharCodes(event).trim();
if (eventStr.isEmpty) {
logger.info('Empty control message (String) received');
return;
}
// TODO The code below (splitting by `connect:`) resolves a
// particular issue for the moment, but the overall approach
// to handling control messages needs to be redone, e.g. :
// Ideally - send the control request, and a newline
// => as of this commit, this is the case
// Receive - wait for newline, handle the request, repeat
// => older npt clients don't send `\n` so we will need to add some
// magic to handle both (a) older clients which don't send `\n`
// as well as (b) newer ones which do. Cleanest is to add a
// flag to the npt request from the client stating that it sends
// `\n` . If so then we handle that cleanly; if not then we use
// this approach (split by `connect:`)
List<String> requests = eventStr.split('connect:');
for (String request in requests) {
if (request.isNotEmpty) {
await _handleMultiConnectRequest(sc, hosts, 'connect:$request');
}
}
} finally {
controlStreamMutex.release();
}
}

Future<SocketConnector> _runDaemonSideSingle({
Expand Down
13 changes: 11 additions & 2 deletions packages/dart/sshnoports/bin/npt.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,13 @@ import 'dart:io';

// other packages
import 'package:args/args.dart';

// atPlatform packages
import 'package:at_cli_commons/at_cli_commons.dart' as cli;
import 'package:at_utils/at_utils.dart';
import 'package:duration/duration.dart';
import 'package:noports_core/npt.dart';
import 'package:noports_core/sshnp_foundation.dart';
import 'package:sshnoports/src/extended_arg_parser.dart';

// local packages
import 'package:sshnoports/src/print_version.dart';

Expand Down Expand Up @@ -203,6 +201,16 @@ void main(List<String> args) async {
' it has started its session.',
);

parser.addFlag(
'encrypt-rvd-traffic',
aliases: ['et'],
help: 'When true, traffic via the socket rendezvous is encrypted,'
' in addition to whatever encryption the traffic already has'
' (e.g. an ssh session)',
defaultsTo: DefaultArgs.encryptRvdTraffic,
negatable: true,
);

// Parse Args
ArgResults parsedArgs = parser.parse(args);

Expand Down Expand Up @@ -321,6 +329,7 @@ void main(List<String> args) async {
inline: inline,
daemonPingTimeout:
Duration(seconds: int.parse(parsedArgs['daemon-ping-timeout'])),
encryptRvdTraffic: parsedArgs['encrypt-rvd-traffic'],
timeout: parseDuration(timeoutArg),
);

Expand Down
2 changes: 1 addition & 1 deletion packages/dart/sshnoports/lib/src/version.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.