diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index fda3f84e3..c1dd7cd4e 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -3910,3 +3910,24 @@ String get appName { } return _appName; } + +String getConnectionText(bool secure, bool direct, String streamType) { + String connectionText; + if (secure && direct) { + connectionText = translate("Direct and encrypted connection"); + } else if (secure && !direct) { + connectionText = translate("Relayed and encrypted connection"); + } else if (!secure && direct) { + connectionText = translate("Direct and unencrypted connection"); + } else { + connectionText = translate("Relayed and unencrypted connection"); + } + if (streamType == 'Relay') { + streamType = 'TCP'; + } + if (streamType.isEmpty) { + return connectionText; + } else { + return '$connectionText ($streamType)'; + } +} diff --git a/flutter/lib/common/shared_state.dart b/flutter/lib/common/shared_state.dart index 908c98a70..4f9373ccd 100644 --- a/flutter/lib/common/shared_state.dart +++ b/flutter/lib/common/shared_state.dart @@ -77,9 +77,11 @@ class CurrentDisplayState { class ConnectionType { final Rx _secure = kInvalidValueStr.obs; final Rx _direct = kInvalidValueStr.obs; + final Rx _stream_type = kInvalidValueStr.obs; Rx get secure => _secure; Rx get direct => _direct; + Rx get stream_type => _stream_type; static String get strSecure => 'secure'; static String get strInsecure => 'insecure'; @@ -94,9 +96,14 @@ class ConnectionType { _direct.value = v ? strDirect : strIndirect; } + void setStreamType(String v) { + _stream_type.value = v; + } + bool isValid() { return _secure.value != kInvalidValueStr && - _direct.value != kInvalidValueStr; + _direct.value != kInvalidValueStr && + _stream_type.value != kInvalidValueStr; } } diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 644f6c336..ba698bd56 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -146,16 +146,8 @@ class _ConnectionTabPageState extends State { connectionType.secure.value == ConnectionType.strSecure; bool direct = connectionType.direct.value == ConnectionType.strDirect; - String msgConn; - if (secure && direct) { - msgConn = translate("Direct and encrypted connection"); - } else if (secure && !direct) { - msgConn = translate("Relayed and encrypted connection"); - } else if (!secure && direct) { - msgConn = translate("Direct and unencrypted connection"); - } else { - msgConn = translate("Relayed and unencrypted connection"); - } + String msgConn = getConnectionText( + secure, direct, connectionType.stream_type.value); var msgFingerprint = '${translate('Fingerprint')}:\n'; var fingerprint = FingerprintState.find(key).value; if (fingerprint.isEmpty) { diff --git a/flutter/lib/desktop/pages/view_camera_tab_page.dart b/flutter/lib/desktop/pages/view_camera_tab_page.dart index 4510949fa..a31ba0fff 100644 --- a/flutter/lib/desktop/pages/view_camera_tab_page.dart +++ b/flutter/lib/desktop/pages/view_camera_tab_page.dart @@ -145,16 +145,8 @@ class _ViewCameraTabPageState extends State { connectionType.secure.value == ConnectionType.strSecure; bool direct = connectionType.direct.value == ConnectionType.strDirect; - String msgConn; - if (secure && direct) { - msgConn = translate("Direct and encrypted connection"); - } else if (secure && !direct) { - msgConn = translate("Relayed and encrypted connection"); - } else if (!secure && direct) { - msgConn = translate("Direct and unencrypted connection"); - } else { - msgConn = translate("Relayed and unencrypted connection"); - } + String msgConn = getConnectionText( + secure, direct, connectionType.stream_type.value); var msgFingerprint = '${translate('Fingerprint')}:\n'; var fingerprint = FingerprintState.find(key).value; if (fingerprint.isEmpty) { diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index b707fd38f..4c8081465 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -40,7 +40,12 @@ void _disableAndroidSoftKeyboard({bool? isKeyboardVisible}) { } class RemotePage extends StatefulWidget { - RemotePage({Key? key, required this.id, this.password, this.isSharedPassword, this.forceRelay}) + RemotePage( + {Key? key, + required this.id, + this.password, + this.isSharedPassword, + this.forceRelay}) : super(key: key); final String id; @@ -1105,7 +1110,7 @@ void showOptions( BuildContext context, String id, OverlayDialogManager dialogManager) async { var displays = []; final pi = gFFI.ffiModel.pi; - final image = gFFI.ffiModel.getConnectionImage(); + final image = gFFI.ffiModel.getConnectionImageText(); if (image != null) { displays.add(Padding(padding: const EdgeInsets.only(top: 8), child: image)); } diff --git a/flutter/lib/mobile/pages/view_camera_page.dart b/flutter/lib/mobile/pages/view_camera_page.dart index 1b668673a..53af56267 100644 --- a/flutter/lib/mobile/pages/view_camera_page.dart +++ b/flutter/lib/mobile/pages/view_camera_page.dart @@ -39,7 +39,11 @@ void _disableAndroidSoftKeyboard({bool? isKeyboardVisible}) { class ViewCameraPage extends StatefulWidget { ViewCameraPage( - {Key? key, required this.id, this.password, this.isSharedPassword, this.forceRelay}) + {Key? key, + required this.id, + this.password, + this.isSharedPassword, + this.forceRelay}) : super(key: key); final String id; @@ -579,7 +583,7 @@ void showOptions( BuildContext context, String id, OverlayDialogManager dialogManager) async { var displays = []; final pi = gFFI.ffiModel.pi; - final image = gFFI.ffiModel.getConnectionImage(); + final image = gFFI.ffiModel.getConnectionImageText(); if (image != null) { displays.add(Padding(padding: const EdgeInsets.only(top: 8), child: image)); } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index c6118efa1..645002686 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -61,6 +61,7 @@ class CachedPeerData { bool secure = false; bool direct = false; + String streamType = ''; CachedPeerData(); @@ -74,6 +75,7 @@ class CachedPeerData { 'permissions': permissions, 'secure': secure, 'direct': direct, + 'streamType': streamType, }); } @@ -92,6 +94,7 @@ class CachedPeerData { }); data.secure = map['secure']; data.direct = map['direct']; + data.streamType = map['streamType']; return data; } catch (e) { debugPrint('Failed to parse CachedPeerData: $e'); @@ -223,27 +226,45 @@ class FfiModel with ChangeNotifier { timerScreenshot?.cancel(); } - setConnectionType(String peerId, bool secure, bool direct) { + setConnectionType( + String peerId, bool secure, bool direct, String streamType) { cachedPeerData.secure = secure; cachedPeerData.direct = direct; + cachedPeerData.streamType = streamType; _secure = secure; _direct = direct; try { var connectionType = ConnectionTypeState.find(peerId); connectionType.setSecure(secure); connectionType.setDirect(direct); + connectionType.setStreamType(streamType); } catch (e) { // } } - Widget? getConnectionImage() { + Widget? getConnectionImageText() { if (secure == null || direct == null) { return null; } else { final icon = '${secure == true ? 'secure' : 'insecure'}${direct == true ? '' : '_relay'}'; - return SvgPicture.asset('assets/$icon.svg', width: 48, height: 48); + final iconWidget = + SvgPicture.asset('assets/$icon.svg', width: 48, height: 48); + String connectionText = + getConnectionText(secure!, direct!, cachedPeerData.streamType); + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + iconWidget, + SizedBox(height: 4), + Text( + connectionText, + style: TextStyle(fontSize: 12), + textAlign: TextAlign.center, + ), + ], + ); } } @@ -260,7 +281,7 @@ class FfiModel with ChangeNotifier { 'link': '', }, sessionId, peerId); updatePrivacyMode(data.updatePrivacyMode, sessionId, peerId); - setConnectionType(peerId, data.secure, data.direct); + setConnectionType(peerId, data.secure, data.direct, data.streamType); await handlePeerInfo(data.peerInfo, peerId, true); for (final element in data.cursorDataList) { updateLastCursorId(element); @@ -289,8 +310,8 @@ class FfiModel with ChangeNotifier { } else if (name == 'sync_platform_additions') { handlePlatformAdditions(evt, sessionId, peerId); } else if (name == 'connection_ready') { - setConnectionType( - peerId, evt['secure'] == 'true', evt['direct'] == 'true'); + setConnectionType(peerId, evt['secure'] == 'true', + evt['direct'] == 'true', evt['stream_type'] ?? ''); } else if (name == 'switch_display') { // switch display is kept for backward compatibility handleSwitchDisplay(evt, sessionId, peerId); diff --git a/src/client.rs b/src/client.rs index 7129e7d9d..438c2ecc6 100644 --- a/src/client.rs +++ b/src/client.rs @@ -192,7 +192,13 @@ impl Client { conn_type: ConnType, interface: impl Interface, ) -> ResultType<( - (Stream, bool, Option>, Option), + ( + Stream, + bool, + Option>, + Option, + &'static str, + ), (i32, String), )> { debug_assert!(peer == interface.get_id()); @@ -219,7 +225,13 @@ impl Client { conn_type: ConnType, interface: impl Interface, ) -> ResultType<( - (Stream, bool, Option>, Option), + ( + Stream, + bool, + Option>, + Option, + &'static str, + ), (i32, String), )> { if config::is_incoming_only() { @@ -234,6 +246,7 @@ impl Client { true, None, None, + "TCP", ), (0, "".to_owned()), )); @@ -246,6 +259,7 @@ impl Client { true, None, None, + "TCP", ), (0, "".to_owned()), )); @@ -257,7 +271,7 @@ impl Client { } else { (peer, "", key, token) }; - let (mut rendezvous_server, servers, contained) = if other_server.is_empty() { + let (rendezvous_server, servers, contained) = if other_server.is_empty() { crate::get_rendezvous_server(1_000).await } else { if other_server == PUBLIC_SERVER { @@ -346,7 +360,13 @@ impl Client { servers: Vec, contained: bool, ) -> ResultType<( - (Stream, bool, Option>, Option), + ( + Stream, + bool, + Option>, + Option, + &'static str, + ), (i32, String), )> { let mut start = Instant::now(); @@ -417,6 +437,12 @@ impl Client { (None, None) }; let udp_nat_port = udp.1.map(|x| *x.lock().unwrap()).unwrap_or(0); + if udp.0.is_some() && udp_nat_port == 0 { + let err_msg = "skip udp punch because udp nat port is 0"; + log::info!("{}", err_msg); + bail!(err_msg); + } + let punch_type = if udp_nat_port > 0 { "UDP" } else { "TCP" }; msg_out.set_punch_hole_request(PunchHoleRequest { id: peer.to_owned(), token: token.to_owned(), @@ -430,7 +456,13 @@ impl Client { ..Default::default() }); for i in 1..=3 { - log::info!("#{} punch attempt with {}, id: {}", i, my_addr, peer); + log::info!( + "#{} {} punch attempt with {}, id: {}", + i, + punch_type, + my_addr, + peer + ); socket.send(&msg_out).await?; // below timeout should not bigger than hbbs's connection timeout. if let Some(msg_in) = @@ -481,7 +513,7 @@ impl Client { } } } - log::info!("Hole Punched {} = {}", peer, peer_addr); + log::info!("{} Hole Punched {} = {}", punch_type, peer, peer_addr); break; } } @@ -529,7 +561,7 @@ impl Client { log::info!("{:?} used to establish {typ} connection", start.elapsed()); let pk = Self::secure_connection(&peer, signed_id_pk, &key, &mut conn).await?; - return Ok(((conn, false, pk, kcp), (feedback, rendezvous_server))); + return Ok(((conn, false, pk, kcp, typ), (feedback, rendezvous_server))); } _ => { log::error!("Unexpected protobuf msg received: {:?}", msg_in); @@ -543,8 +575,9 @@ impl Client { } let time_used = start.elapsed().as_millis() as u64; log::info!( - "{} ms used to punch hole, relay_server: {}, {}", + "{} ms used to {} punch hole, relay_server: {}, {}", time_used, + punch_type, relay_server, if is_local { "is_local: true".to_owned() @@ -570,6 +603,7 @@ impl Client { interface, udp.0, ipv6.0, + punch_type, ) .await?, (feedback, rendezvous_server), @@ -594,7 +628,14 @@ impl Client { interface: impl Interface, udp_socket_nat: Option>, udp_socket_v6: Option>, - ) -> ResultType<(Stream, bool, Option>, Option)> { + punch_type: &str, + ) -> ResultType<( + Stream, + bool, + Option>, + Option, + &'static str, + )> { let direct_failures = interface.get_lch().read().unwrap().direct_failures; let mut connect_timeout = 0; const MIN: u64 = 1000; @@ -681,9 +722,14 @@ impl Client { interface.get_lch().write().unwrap().set_direct_failure(n); } let mut conn = conn?; - log::info!("{:?} used to establish {typ} connection", start.elapsed()); + log::info!( + "{:?} used to establish {typ} connection with {} punch", + start.elapsed(), + punch_type + ); let pk = Self::secure_connection(peer_id, signed_id_pk, key, &mut conn).await?; - Ok((conn, direct, pk, kcp)) + log::info!("{} punch secure_connection ok", punch_type); + Ok((conn, direct, pk, kcp, typ)) } /// Establish secure connection with the server. diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 9ed96365d..878a227f2 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -174,13 +174,14 @@ impl Remote { ) .await { - Ok(((mut peer, direct, pk, kcp), (feedback, rendezvous_server))) => { + Ok(((mut peer, direct, pk, kcp, stream_type), (feedback, rendezvous_server))) => { self.handler .connection_round_state .lock() .unwrap() .set_connected(); - self.handler.set_connection_type(peer.is_secured(), direct); // flutter -> connection_ready + self.handler + .set_connection_type(peer.is_secured(), direct, stream_type); // flutter -> connection_ready self.handler.update_direct(Some(direct)); if conn_type == ConnType::DEFAULT_CONN || conn_type == ConnType::VIEW_CAMERA { self.handler diff --git a/src/flutter.rs b/src/flutter.rs index 602f5701a..198d68505 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -716,12 +716,13 @@ impl InvokeUiSession for FlutterHandler { ); } - fn set_connection_type(&self, is_secured: bool, direct: bool) { + fn set_connection_type(&self, is_secured: bool, direct: bool, stream_type: &str) { self.push_event( "connection_ready", &[ ("secure", &is_secured.to_string()), ("direct", &direct.to_string()), + ("stream_type", &stream_type.to_string()), ], &[], ); diff --git a/src/port_forward.rs b/src/port_forward.rs index bd4b9fb78..056233b00 100644 --- a/src/port_forward.rs +++ b/src/port_forward.rs @@ -118,7 +118,7 @@ async fn connect_and_login( } else { ConnType::PORT_FORWARD }; - let ((mut stream, direct, _pk, _kcp), (feedback, rendezvous_server)) = + let ((mut stream, direct, _pk, _kcp, _stream_type), (feedback, rendezvous_server)) = Client::start(id, key, token, conn_type, interface.clone()).await?; interface.update_direct(Some(direct)); let mut buffer = Vec::new(); diff --git a/src/ui/header.tis b/src/ui/header.tis index 305248b45..17efe6982 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -117,6 +117,13 @@ class Header: Reactor.Component { icon_conn = svg_insecure_relay; title_conn = translate("Relayed and unencrypted connection"); } + var stream_type = this.stream_type; + if (stream_type == "Relay") { + stream_type = "TCP"; + } + if (stream_type) { + title_conn += " (" + stream_type + ")"; + } var title = get_id(); if (pi.hostname) title += "(" + pi.username + "@" + pi.hostname + ")"; if ((pi.displays || []).length == 0) { @@ -695,10 +702,11 @@ function startChat() { chatbox = view.window(params); } -handler.setConnectionType = function(secured, direct) { +handler.setConnectionType = function(secured, direct, stream_type) { header.update({ secure_connection: secured, direct_connection: direct, + stream_type: stream_type, }); } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index f99e2de6e..f67f37902 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -178,8 +178,11 @@ impl InvokeUiSession for SciterHandler { self.call("setCursorPosition", &make_args!(cp.x, cp.y)); } - fn set_connection_type(&self, is_secured: bool, direct: bool) { - self.call("setConnectionType", &make_args!(is_secured, direct)); + fn set_connection_type(&self, is_secured: bool, direct: bool, stream_type: &str) { + self.call( + "setConnectionType", + &make_args!(is_secured, direct, stream_type.to_string()), + ); } fn set_fingerprint(&self, _fingerprint: String) {} diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 93dde3909..fcb84da21 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -192,7 +192,11 @@ impl Session { } pub fn is_default(&self) -> bool { - self.lc.read().unwrap().conn_type.eq(&ConnType::DEFAULT_CONN) + self.lc + .read() + .unwrap() + .conn_type + .eq(&ConnType::DEFAULT_CONN) } pub fn is_view_camera(&self) -> bool { @@ -804,7 +808,6 @@ impl Session { self.send(Data::Message(msg_out)); } - pub fn capture_displays(&self, add: Vec, sub: Vec, set: Vec) { let mut misc = Misc::new(); misc.set_capture_displays(CaptureDisplays { @@ -1611,7 +1614,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn set_permission(&self, name: &str, value: bool); fn close_success(&self); fn update_quality_status(&self, qs: QualityStatus); - fn set_connection_type(&self, is_secured: bool, direct: bool); + fn set_connection_type(&self, is_secured: bool, direct: bool, stream_type: &str); fn set_fingerprint(&self, fingerprint: String); fn job_error(&self, id: i32, err: String, file_num: i32); fn job_done(&self, id: i32, file_num: i32);