diff --git a/app/src/main/java/com/foxx/androidcast/AppPreferences.java b/app/src/main/java/com/foxx/androidcast/AppPreferences.java index c9a8c71..6ee9f4a 100644 --- a/app/src/main/java/com/foxx/androidcast/AppPreferences.java +++ b/app/src/main/java/com/foxx/androidcast/AppPreferences.java @@ -69,6 +69,10 @@ public final class AppPreferences { private static final String KEY_DEV_REMOTE_ACCESS_RANDOM = "dev_remote_access_random"; private static final String KEY_DEV_REMOTE_ACCESS_SESSION_ID = "dev_remote_access_session_id"; private static final String KEY_DEV_REMOTE_ACCESS_EXPIRES_AT = "dev_remote_access_expires_at"; + private static final String KEY_DEV_REMOTE_ACCESS_VPN_ADDRESS = "dev_remote_access_vpn_address"; + private static final String KEY_DEV_REMOTE_ACCESS_RSSH_BASTION = "dev_remote_access_rssh_bastion"; + private static final String KEY_DEV_REMOTE_ACCESS_RSSH_REMOTE_BIND = "dev_remote_access_rssh_remote_bind"; + private static final String KEY_DEV_REMOTE_ACCESS_RSSH_LOCAL_FORWARD = "dev_remote_access_rssh_local_forward"; private static final String KEY_DEV_REMOTE_ACCESS_VPN_ROUTE_SCOPE = "dev_remote_access_vpn_route_scope"; private static final String KEY_DEV_REMOTE_ACCESS_VPN_APP_SCOPE = "dev_remote_access_vpn_app_scope"; private static final String KEY_DEV_ADB_WIFI_KEEPER = "dev_adb_wifi_keeper"; @@ -519,11 +523,73 @@ public final class AppPreferences { .apply(); } + /** WG interface address from last connect credentials (host part, no CIDR). */ + public static String getDevRemoteAccessVpnAddress(Context context) { + return prefs(context).getString(KEY_DEV_REMOTE_ACCESS_VPN_ADDRESS, ""); + } + + public static void setDevRemoteAccessVpnAddress(Context context, String addressCidr) { + String host = addressCidr != null ? addressCidr.trim() : ""; + int slash = host.indexOf('/'); + if (slash > 0) { + host = host.substring(0, slash); + } + prefs(context).edit() + .putString(KEY_DEV_REMOTE_ACCESS_VPN_ADDRESS, host) + .apply(); + } + + public static String getDevRemoteAccessRsshBastionEndpoint(Context context) { + return prefs(context).getString(KEY_DEV_REMOTE_ACCESS_RSSH_BASTION, ""); + } + + /** Bastion-side operator bind (127.0.0.1:port) from last RSSH connect. */ + public static String getDevRemoteAccessRsshRemoteBind(Context context) { + return prefs(context).getString(KEY_DEV_REMOTE_ACCESS_RSSH_REMOTE_BIND, ""); + } + + public static String getDevRemoteAccessRsshLocalForward(Context context) { + return prefs(context).getString(KEY_DEV_REMOTE_ACCESS_RSSH_LOCAL_FORWARD, ""); + } + + public static void setDevRemoteAccessRsshTunnelMeta( + Context context, + String bastionHost, + int bastionPort, + int remoteBindPort, + String localForwardHost, + int localForwardPort) { + String host = bastionHost != null ? bastionHost.trim() : ""; + String bastion = host.isEmpty() + ? "" + : host + ":" + Math.max(1, Math.min(65535, bastionPort)); + String remoteBind = remoteBindPort > 0 ? "127.0.0.1:" + remoteBindPort : ""; + String localHost = localForwardHost != null && !localForwardHost.isEmpty() + ? localForwardHost.trim() : "127.0.0.1"; + String localForward = localForwardPort > 0 ? localHost + ":" + localForwardPort : ""; + prefs(context).edit() + .putString(KEY_DEV_REMOTE_ACCESS_RSSH_BASTION, bastion) + .putString(KEY_DEV_REMOTE_ACCESS_RSSH_REMOTE_BIND, remoteBind) + .putString(KEY_DEV_REMOTE_ACCESS_RSSH_LOCAL_FORWARD, localForward) + .apply(); + } + + /** Clears WG/RSSH tunnel endpoint fields from the last connect (not mode/route prefs). */ + public static void clearDevRemoteAccessTunnelMeta(Context context) { + prefs(context).edit() + .remove(KEY_DEV_REMOTE_ACCESS_VPN_ADDRESS) + .remove(KEY_DEV_REMOTE_ACCESS_RSSH_BASTION) + .remove(KEY_DEV_REMOTE_ACCESS_RSSH_REMOTE_BIND) + .remove(KEY_DEV_REMOTE_ACCESS_RSSH_LOCAL_FORWARD) + .apply(); + } + public static void clearDevRemoteAccessSession(Context context) { prefs(context).edit() .remove(KEY_DEV_REMOTE_ACCESS_SESSION_ID) .remove(KEY_DEV_REMOTE_ACCESS_EXPIRES_AT) .apply(); + clearDevRemoteAccessTunnelMeta(context); } public static long getDevRemoteAccessExpiresAt(Context context) { diff --git a/app/src/main/java/com/foxx/androidcast/dev/DevVpnStatusProbe.java b/app/src/main/java/com/foxx/androidcast/dev/DevVpnStatusProbe.java index 990b916..e1816d5 100644 --- a/app/src/main/java/com/foxx/androidcast/dev/DevVpnStatusProbe.java +++ b/app/src/main/java/com/foxx/androidcast/dev/DevVpnStatusProbe.java @@ -43,7 +43,7 @@ public final class DevVpnStatusProbe { String typeLabel = mode == RemoteAccessMode.DISABLED ? "off" : mode.name().toLowerCase(Locale.US); r.itemPass("type", typeLabel); - if (mode != RemoteAccessMode.DISABLED) { + if (mode == RemoteAccessMode.WIREGUARD) { r.itemPass( "vpn-route-scope", RemoteAccessVpnRouting.routeScopeLabel( @@ -78,6 +78,24 @@ public final class DevVpnStatusProbe { String tunnelLabel = formatTunnelStateLabel(tunnelState, last); r.item(tunnelLevel, "tunnel-state", tunnelLabel); + if (mode == RemoteAccessMode.WIREGUARD) { + String vpnIp = AppPreferences.getDevRemoteAccessVpnAddress(context); + if (!vpnIp.isEmpty()) { + r.itemPass("vpn-ip", vpnIp); + } else if (RemoteAccessStatusStore.STATE_SUCCESS.equals(tunnelState) + || RemoteAccessStatusStore.STATE_CONNECTING.equals(tunnelState)) { + r.item(DevNetworkSelfTestReport.Level.WARN, "vpn-ip", "not stored (reconnect session)"); + } + if (AppPreferences.getDevRemoteAccessVpnRouteScope(context) + == RemoteAccessVpnRouting.RouteScope.HUB_ONLY) { + r.itemPass( + "wan-egress-via-vpn", + "no (hub-only — only " + RemoteAccessVpnRouting.DEFAULT_HUB_ALLOWED_IPS + ")"); + } + } else if (mode == RemoteAccessMode.RSSH) { + appendRsshTunnelMeta(context, r, tunnelState); + } + if (mode != RemoteAccessMode.DISABLED && !RemoteAccessStatusStore.STATE_OFF.equals(controlPlane)) { DevNetworkSelfTestReport.Level cpLevel; @@ -98,6 +116,8 @@ public final class DevVpnStatusProbe { if (mainProcess && mode == RemoteAccessMode.WIREGUARD) { boolean bound = VpnServiceClient.get(context).isBound(); r.itemPass("vpn-process-bound", bound ? "yes (:vpn AIDL)" : "no (not bound — normal if idle)"); + } else if (mainProcess && mode == RemoteAccessMode.RSSH) { + r.itemPass("rssh-connected", ReverseSshTunnelBridge.isConnected() ? "yes" : "no"); } if (!vpnDetail.isEmpty()) { r.itemPass("detail", vpnDetail); @@ -304,6 +324,33 @@ public final class DevVpnStatusProbe { return caps != null && caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN); } + private static void appendRsshTunnelMeta( + Context context, + DevNetworkSelfTestReport r, + String tunnelState) { + String bastion = AppPreferences.getDevRemoteAccessRsshBastionEndpoint(context); + if (!bastion.isEmpty()) { + r.itemPass("rssh-bastion", bastion); + } else if (RemoteAccessStatusStore.STATE_SUCCESS.equals(tunnelState) + || RemoteAccessStatusStore.STATE_CONNECTING.equals(tunnelState)) { + r.item(DevNetworkSelfTestReport.Level.WARN, "rssh-bastion", "not stored (reconnect session)"); + } + String remoteBind = AppPreferences.getDevRemoteAccessRsshRemoteBind(context); + if (!remoteBind.isEmpty()) { + r.itemPass("rssh-remote-bind", remoteBind); + } else if (RemoteAccessStatusStore.STATE_SUCCESS.equals(tunnelState) + || RemoteAccessStatusStore.STATE_CONNECTING.equals(tunnelState)) { + r.item(DevNetworkSelfTestReport.Level.WARN, "rssh-remote-bind", "not stored (reconnect session)"); + } + String localForward = AppPreferences.getDevRemoteAccessRsshLocalForward(context); + if (!localForward.isEmpty()) { + r.itemPass("rssh-local-forward", localForward); + } + r.itemPass( + "wan-egress-via-vpn", + "no (RSSH — reverse SSH only, no device routing)"); + } + private static void appendTraffic(DevNetworkSelfTestReport r, Context context) { int uid = Process.myUid(); long rx = TrafficStats.getUidRxBytes(uid); diff --git a/app/src/main/java/com/foxx/androidcast/remoteaccess/RemoteAccessService.java b/app/src/main/java/com/foxx/androidcast/remoteaccess/RemoteAccessService.java index 98cdc45..3e6222b 100644 --- a/app/src/main/java/com/foxx/androidcast/remoteaccess/RemoteAccessService.java +++ b/app/src/main/java/com/foxx/androidcast/remoteaccess/RemoteAccessService.java @@ -219,6 +219,19 @@ public final class RemoteAccessService extends Service { "connect without credentials", sessionId); return; } + AppPreferences.clearDevRemoteAccessTunnelMeta(this); + if ("wireguard".equals(tunnel)) { + AppPreferences.setDevRemoteAccessVpnAddress( + this, creds.optString("interface_address", "")); + } else if ("ssh_reverse".equals(tunnel)) { + AppPreferences.setDevRemoteAccessRsshTunnelMeta( + this, + creds.optString("bastion_host", ""), + creds.optInt("bastion_port", 443), + creds.optInt("remote_bind_port", 0), + creds.optString("local_forward_host", "127.0.0.1"), + creds.optInt("local_forward_port", 8022)); + } if ("wireguard".equals(tunnel)) { String wgConfig = WireGuardConfigBuilder.fromConnectCredentials(this, creds); if (wgConfig.isEmpty()) {