对于所有后端开发人员来说,他们可以通过ssh登录到服务端,查看服务端的日志,分析问题,有时候还需要开实时的日志,如果我们Android端也能有类似的功能那该有多啊,去年一devops朋友推荐我用过一个内网穿透的软件frp,自己也搭过一次,感觉不错,所以能将fprc和ssh移植到android平台的话,这一切都是小case

初步方案确定

在GitHub和Google搜索后,FRPC有前辈已经用gomobile将frpc移植到Android平台,同时在Google Play上也找到了一个开源的ssh-server Android端
两个项目的地址如下:

frpc-Android

simplesshd

原理

frp原理

FRP原理

frp服务端配置和frpc代码移植

一般需要在服务端配置

1
2
3
4
root@JD:~/software/frp_0.30.0_linux_amd64# cat frps.ini
[common]
bind_port = 61234
token = @12345

客户端需要通过api或者推送拿到如下参数

1
2
3
4
5
server_addr : xxx.xxxx.xxxx
port : 61235
token : xxxxx
remote_port : 61234
local_port : 11111

参数解释

hostname : 公网主机地址。

port : 内网映射到外网的端口,local_port的流量转发到frpc监听remote_port,fprc与服务端建立通讯,我们会那这个这端口来连ssh。

token : 客户端向frps认证需要的token

remote_port : frps监听的端口用于认证

local_port : 暴露给局域网的端口

客户端根据这5个参数生成一个符合frpc.ini规范的文件
例如下面的

1
2
3
4
5
6
7
8
9
10
[common]
server_addr = 127.0.0.1
server_port = 7000
token = xxxxx

[ssh]
type = tcp
local_ip = 127.0.0.1
local_port = 22
remote_port = 6000

通过go mobile 编译出来的jni接口如下,将生成的文件路径传入即可

1
2
3
4
5
6
7
8
9
10
11
12
13
package frpclib;
import go.Seq;
public abstract class Frpclib {
static {
Seq.touch(); // for loading the native library
_init();
}

private Frpclib() {} // uninstantiable
public static void touch() {}
private static native void _init();
public static native void run(String cfgFilePath);
}

而fprc上改的也只是将cmd/frpc/sub/root.go下的func runClient(cfgFilePath string) (err error)改为了外部可访问的方法func RunClient(cfgFilePath string) (err error)
然而在cmd/frpc/main.go 添加了一个公共方法调用RunClient(cfgFilePath string)
如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package frpclib

import (
_ "github.com/fatedier/frp/assets/frpc/statik"
"github.com/fatedier/frp/cmd/frpc/sub"

"github.com/fatedier/golib/crypto"
)

func main() {
crypto.DefaultSalt = "frp"

sub.Execute()
}

func Run(cfgFilePath string) {
crypto.DefaultSalt = "frp"
sub.RunClient(cfgFilePath)
}

所以public static native void run(String cfgFilePath);这里需要传递之前生成配置文件的路径即可。

sshd相关方法

而sshd的主要native方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package org.galexander.sshd.SimpleSSHDService;
public class SimpleSSHDService extends Service {
private static native int start_sshd(
int port, // 端口与上方local_port保持一致
String path, // 错误日志路径
String shell, // 连接执行的第一条命令默认/system/bin/sh
String home, // 连接所在路径,默认/data/data/{applicationId}/files/
String extra, // ""
int rsyncbuffer, // 是否开启sftp服务,这个要开启,设置1
String env, // ""
String lib // nativelib路径,如果设置错sftp服务不能用,猜测应该是动态加载so的方式来弄得
);

private static native void kill(int pid);

private static native int waitpid(int pid);

static {
System.loadLibrary("simplesshd-jni");
}
}

这里正确和调用start_sshd()后,会将进程id写入到dropbear.pid文件里,kill()也是读这个文件,传入pid.

主要流程

流程如下:

  1. 轮训API或者等待推送到来
  2. 有消息来后解析消息,然后启动frps,将sshd启动起来
  3. 有停止消息的话,停止frps,sshd
  4. 如果是轮训的方式,需要将客户端的两个服务的状态回传给服务器,服务收到对于的状态就不再下发消息