1
0
mirror of git://f0xx.org/android_cast synced 2026-07-29 04:18:09 +03:00

some rssh stuff

This commit is contained in:
Anton Afanasyeu
2026-06-15 13:34:19 +02:00
parent d29c212297
commit 066e21a3f2
6 changed files with 92 additions and 14 deletions

View File

@@ -119,6 +119,7 @@ android {
excludes += '/META-INF/LICENSE.txt'
excludes += '/META-INF/NOTICE'
excludes += '/META-INF/NOTICE.txt'
excludes += '/META-INF/versions/**'
}
}
@@ -146,6 +147,8 @@ dependencies {
implementation 'com.github.mwiede:jsch:0.2.21'
implementation 'org.apache.sshd:sshd-core:2.14.0'
implementation 'org.apache.sshd:sshd-sftp:2.14.0'
implementation 'org.bouncycastle:bcprov-jdk18on:1.78.1'
implementation 'org.slf4j:slf4j-android:1.7.36'
implementation project(':tunnel')

View File

@@ -19,7 +19,8 @@
<li><b>Google Play In-App Review</b> — Google Play SDK terms</li>
<li><b>Eclipse Paho MQTT client</b> — Eclipse Public License 1.0 and Eclipse Distribution License 1.0 (dual-licensed)</li>
<li><b>JSch (mwiede fork)</b> — BSD 3-Clause License (reverse SSH remote access)</li>
<li><b>Apache MINA SSHD</b> — Apache License 2.0 (embedded local SSH on device for RSSH alpha)</li>
<li><b>Apache MINA SSHD</b> — Apache License 2.0 (embedded local SSH/SFTP for RSSH alpha)</li>
<li><b>Bouncy Castle</b> — MIT-style license (SSHD crypto on Android)</li>
</ul>
<h2>JUnit 4</h2>

View File

@@ -42,7 +42,7 @@ public final class ReverseSshTunnelBridge {
Log.w(TAG, "incomplete RSSH credentials");
return false;
}
if (!RsshLocalSshServer.start(user, pass, localPort)) {
if (!RsshLocalSshServer.start(context, user, pass, localPort)) {
Log.w(TAG, "local SSH on :" + localPort + " failed");
return false;
}

View File

@@ -1,18 +1,19 @@
package com.foxx.androidcast.remoteaccess;
import android.content.Context;
import android.util.Log;
import org.apache.sshd.server.SshServer;
import org.apache.sshd.server.auth.password.PasswordAuthenticator;
import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
import org.apache.sshd.server.shell.ProcessShellFactory;
import org.apache.sshd.sftp.server.SftpSubsystemFactory;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Embedded SSH on {@code 127.0.0.1:8022} for RSSH reverse forward target.
* Embedded SSH/SFTP on {@code 127.0.0.1:8022} for RSSH reverse forward target.
* Operator reaches this via bastion forwarded port (same username/password).
*/
public final class RsshLocalSshServer {
@@ -24,9 +25,10 @@ public final class RsshLocalSshServer {
private RsshLocalSshServer() {}
public static boolean start(String username, String password, int port) {
public static boolean start(Context context, String username, String password, int port) {
stop();
if (username == null || username.isEmpty() || password == null || password.isEmpty()) {
if (context == null || username == null || username.isEmpty()
|| password == null || password.isEmpty()) {
return false;
}
if (port <= 0 || port > 65535) {
@@ -36,8 +38,8 @@ public final class RsshLocalSshServer {
final String pass = password;
final int listenPort = port;
try {
Path hostKey = Files.createTempFile("rssh_host", ".key");
hostKey.toFile().deleteOnExit();
RsshSecuritySetup.ensureReady(context);
java.nio.file.Path hostKey = context.getCacheDir().toPath().resolve("rssh_host.key");
SshServer sshd = SshServer.setUpDefaultServer();
sshd.setHost("127.0.0.1");
sshd.setPort(listenPort);
@@ -45,14 +47,15 @@ public final class RsshLocalSshServer {
sshd.setPasswordAuthenticator((PasswordAuthenticator) (u, p, session) ->
user.equals(u) && pass.equals(p));
sshd.setShellFactory(new ProcessShellFactory("/system/bin/sh", "-"));
sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory()));
sshd.start();
server = sshd;
activeUser = user;
running.set(true);
Log.i(TAG, "local SSH listening 127.0.0.1:" + listenPort + " user=" + user);
Log.i(TAG, "local SSH/SFTP listening 127.0.0.1:" + listenPort + " user=" + user);
return true;
} catch (Exception e) {
Log.w(TAG, "local SSH start failed: " + e.getMessage());
} catch (Throwable e) {
Log.e(TAG, "local SSH start failed: " + e.getMessage(), e);
stop();
return false;
}

View File

@@ -0,0 +1,45 @@
package com.foxx.androidcast.remoteaccess;
import android.content.Context;
import android.util.Log;
import org.apache.sshd.common.util.OsUtils;
import org.apache.sshd.common.util.io.PathUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.nio.file.Path;
import java.security.Security;
import java.util.concurrent.atomic.AtomicBoolean;
/** One-time MINA SSHD + BouncyCastle setup for Android (see mina-sshd docs/android.md). */
final class RsshSecuritySetup {
private static final String TAG = "RsshSecurity";
private static final AtomicBoolean READY = new AtomicBoolean(false);
private RsshSecuritySetup() {}
static void ensureReady(Context context) {
if (!READY.compareAndSet(false, true)) {
return;
}
Context app = context.getApplicationContext();
try {
try {
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME);
} catch (Exception ignored) {
}
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
Security.addProvider(new BouncyCastleProvider());
}
Path home = app.getFilesDir().toPath();
System.setProperty("user.home", home.toString());
PathUtils.setUserHomeFolderResolver(() -> home);
OsUtils.setCurrentUser(app.getPackageName());
Log.i(TAG, "SSHD security providers ready; user.home=" + home);
} catch (Exception e) {
READY.set(false);
Log.e(TAG, "SSHD security setup failed: " + e.getMessage(), e);
throw new IllegalStateException("RSSH security setup failed", e);
}
}
}

View File

@@ -518,8 +518,16 @@
const rowKey = 'ra-' + i;
const briefId = 'ra-brief-' + rowKey;
const wl = Number(d.whitelisted) === 1;
const canOpen = canOperate() && wl && (d.opt_in_mode === 'wireguard' || d.opt_in_mode === 'rssh');
const openDisabled = canOpen ? '' : ' disabled title="Whitelist device and wait for WireGuard/RSSH opt-in poll"';
const optIn = d.opt_in_mode || 'none';
const openReady = wl && (optIn === 'wireguard' || optIn === 'rssh');
const openHint = !canOperate()
? 'Need remote_access_operate permission'
: !wl
? 'Whitelist device first'
: openReady
? 'Open session — device connects on next poll (≤7 min)'
: 'Phone must poll with RSSH/WG enabled (dev settings on device; wait ≤7 min)';
const openDisabled = canOperate() ? '' : ' disabled';
const wlDisabled = canAdmin() ? '' : ' disabled';
const isOpen = lastExpanded.has(String(d.device_id || ''));
const rowClass =
@@ -545,6 +553,8 @@
openDisabled +
' data-open-session="' +
esc(d.device_id) +
'" title="' +
esc(openHint) +
'">Open session</button> ' +
'<button type="button" class="btn btn-sm"' +
wlDisabled +
@@ -691,6 +701,22 @@
const openId = t.getAttribute('data-open-session');
if (openId) {
if (!canOperate()) return;
const dev = (lastDevices || []).find((d) => String(d.device_id) === openId);
const wl = dev && Number(dev.whitelisted) === 1;
const optIn = dev ? dev.opt_in_mode || 'none' : 'none';
if (!wl) {
setStatus('Whitelist device ' + openId + ' first', true);
return;
}
if (optIn !== 'wireguard' && optIn !== 'rssh') {
setStatus(
'Device opt-in is "' +
optIn +
'". On phone: dev settings → Remote access → RSSH, then wait for poll (≤7 min).',
true
);
return;
}
try {
setStatus('Opening session for ' + openId + '…');
await fetchJson(apiUrl('open_session'), {