0%

ja-netfilter 设计与实现

ja-netfilter 是一款 JavaAgent 框架,而 JavaAgent 是由 Java 提供一种动态代理机制,可以在 runtime 时修改 Class 字节码,从而实现代码注入操作。

ja-netfilter 对底层操作进行封装,提供插件系统(Plugin System)机制来向外部暴露简易的接口,使得注入操作更加的简单。

example

releases page 下载 ja-netfilter 最新的发布版本,解压该压缩包,其目录结构如下:

  1. plugins:插件 jar 目录
  2. config:插件配置目录

配置名称需要与插件 jar 名称相同

Snipaste_2022-01-29_10-50-29

插件需要引入 ja-netfilter 作为依赖,然后实现其 PluginEntry 与 MyTransformer 接口,前者用于描述插件信息,后者则对字节码进行转换操作,换句话说,在这过程中可以对字节码进行注入。

.java 文件编译成 class 字节码后,通过 java 命令来运行,而该命令提供了一个 -javaagent 参数,用于在 Runtime 时对字节码进行操作。启动 ja-netfilter 需要添加 -javaagent:/absolute/path/to/ja-netfilter.jar 在命令参数后,完整的命令应是这样 java -javaagent:/absolute/path/to/ja-netfilter.jar -jar executable_jar_file.jar。

命令执行后,ja-netfilter 会随着虚拟机启动后进行调用,从 plugins 目录加载插件,根据插件的行为进行注入操作。

implement

在 pom.xml 的 maven-assembly-plugin 配置中,会添加如下的配置来限定 javaagent 入口类,根据如下配置,当 javaagent 启动时,会调用其 Launcher 类的 agentmain 方法。

JDK 文档指出,需在 manifest 文件指定 javaagent entry class,javaagent 会调用 entry class 的 agentmain 方法

see more details:https://docs.oracle.com/javase/7/docs/api/java/lang/instrument/package-summary.html

1
2
3
4
5
6
7
8
9
<manifestEntries>
<Built-By>neo</Built-By>
<Premain-Class>com.janetfilter.core.Launcher</Premain-Class>
<Agent-Class>com.janetfilter.core.Launcher</Agent-Class>
<Main-Class>com.janetfilter.core.Launcher</Main-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
<Can-Set-Native-Method-Prefix>true</Can-Set-Native-Method-Prefix>
</manifestEntries>

看到 agentmain 方法,倘若开启了 debug mode,设置 debug 参数之后,随后就调用内部的 premain 方法。

1
2
3
4
5
6
7
public static void agentmain(String args, Instrumentation inst) {
if (null == System.getProperty("janf.debug")) {
System.setProperty("janf.debug", "1");
}

premain(args, inst, true);
}

premain 方法中,主要逻辑如下所示:

  1. 判断是否多次加载
  2. 打印 usage
  3. 获取到该 jar,用于 BootStrap Class Loader 进行搜索
  4. 创建 Environment 实例,初始化配置环境
  5. 调用 Initializer 的 init 方法进行后续初始化

1-5 都是一些前置操作,我们关注点应放在 init 方法上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
private static void premain(String args, Instrumentation inst, boolean attachMode) {
// 判断是否加载多次
if (loaded) {
DebugInfo.warn("You have multiple `ja-netfilter` as javaagent.");
return;
}

// 打印 usage
printUsage();

URI jarURI;
try {
loaded = true;
jarURI = WhereIsUtils.getJarURI();
} catch (Throwable e) {
DebugInfo.error("Can not locate `ja-netfilter` jar file.", e);
return;
}

// 获取该 jar 所在的文件
File agentFile = new File(jarURI.getPath());
try {
inst.appendToBootstrapClassLoaderSearch(new JarFile(agentFile));
} catch (Throwable e) {
DebugInfo.error("Can not access `ja-netfilter` jar file.", e);
return;
}

// 解析程序参数并初始化
Initializer.init(inst, new Environment(agentFile, args, attachMode)); // for some custom UrlLoaders
}

来到 init 方法,根据 environment 来创建 Dispatcher 实例,该 Dispatcher 设计运用了委托者模式,用于衔接 javaagent 与 ja-netfilter。随后创建 PluginManager 实例来加载 plugin 插件,并将 dispatcher 添加到 Instrumentation 中,因 dispatcher 实现了 ClassFileTransformer 接口,当进行 transformer 操作时候,会调用其 transform 方法。

之后根据 Dispatcher 的 getHookClassNames 来获取插件需要 HOOK 的类名,并遍历 Instrumentation 所有加载的类,如果类符合 HOOK 要求,那么调用其的 retransformClasses 方法进行 transformer 操作,最后来到 Dispatcher 的 transform 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// com.janetfilter.core.Initializer#init
public static void init(Instrumentation inst, Environment environment) {
DebugInfo.useFile(environment.getLogsDir());
DebugInfo.info(environment.toString());

Dispatcher dispatcher = new Dispatcher(environment);
// 加载 plugins 目录下的插件
new PluginManager(inst, dispatcher, environment).loadPlugins();

// 添加 dispatcher 作为整个拦截的入口点
inst.addTransformer(dispatcher, true);
inst.setNativeMethodPrefix(dispatcher, environment.getNativePrefix());

// 获取需要被 hook 的 class name
Set<String> classSet = dispatcher.getHookClassNames();
for (Class<?> c : inst.getAllLoadedClasses()) {
String name = c.getName();
// 当前 class 不需要被 hook
if (!classSet.contains(name)) {
continue;
}

try {
// 交由 Dispatcher 进行 hook 操作
// 内部会调用其 transform 方法
inst.retransformClasses(c);
} catch (Throwable e) {
DebugInfo.error("Retransform class failed: " + name, e);
}
}
}

加载插件

PluginManager 的 loadPlugins 方法目的是为了从 plugins 目录下加载插件,首先获取到插件目录,之后加载目录下所有文件进行一个过滤操作,为了扫描出其中所有的 jar 包文件。

因加载插件是一个耗时操作,所以采用了线程池设计,每个 plugin file 会被包装成 PluginLoadTask 提交给线程 池,执行时会调用其 run 方法。当所有 task 都提交到线程池,会进行 shutdown 操作,随后等待线程池 30s 终止,倘若执行超时,则判定加载插件超时。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// com.janetfilter.core.plugin.PluginManager#loadPlugins
public void loadPlugins() {
long startTime = System.currentTimeMillis();

File pluginsDirectory = environment.getPluginsDir();
// plugins 目录不存在或 plugins 不是目录
if (!pluginsDirectory.exists() || !pluginsDirectory.isDirectory()) {
return;
}

// 扫描 plugins 所有 jar 包
File[] pluginFiles = pluginsDirectory.listFiles((d, n) -> n.endsWith(".jar"));
if (null == pluginFiles) {
return;
}

try {
ExecutorService executorService = Executors.newCachedThreadPool();
// 向线程池提交 task 来加载插件
for (File pluginFile : pluginFiles) {
executorService.submit(new PluginLoadTask(pluginFile));
}

executorService.shutdown();
if (!executorService.awaitTermination(30L, TimeUnit.SECONDS)) {
throw new RuntimeException("Load plugin timeout");
}

DebugInfo.debug(String.format("============ All plugins loaded, %.2fs elapsed ============", (System.currentTimeMillis() - startTime) / 1000D));
} catch (Throwable e) {
DebugInfo.error("Load plugin failed", e);
}
}

如上所述,加载插件逻辑会放在 PluginLoadTask 的 run 方法中。首先获取该 plugin jar 的 manifest 信息,从而获取 jar 的 entry class name,后续由 PluginClassLoader 来将 class name 加载成 class,如果该 entry class 未实现 PluginEntry 接口,说明该 jar 不是 plugin,则不需要后续操作。

获取插件的配置文件后,解析并用 PluginConfig 进行表示,然后调用 PluginEntry#init 对插件进行初始化,最后将 Plugin 的 MyTransformer 实现添加到 Dispatcher 中,从而完成插件加载化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public void run() {
try {
if (pluginFile.getName().endsWith(environment.getDisabledPluginSuffix())) {
DebugInfo.debug("Disabled plugin: " + pluginFile + ", ignored.");
return;
}

JarFile jarFile = new JarFile(pluginFile);
Manifest manifest = jarFile.getManifest();
// 从 jar 的 manifest 配置文件中获取 entry 类
String entryClass = manifest.getMainAttributes().getValue(ENTRY_NAME);
if (StringUtils.isEmpty(entryClass)) {
return;
}

PluginClassLoader classLoader = new PluginClassLoader(jarFile);
Class<?> klass = Class.forName(entryClass, false, classLoader);
// entry class 未实现 PluginEntry 接口,说明这不是个 Plugin,那么直接 return
if (!Arrays.asList(klass.getInterfaces()).contains(PluginEntry.class)) {
return;
}

synchronized (inst) {
inst.appendToBootstrapClassLoaderSearch(jarFile);
}

PluginEntry pluginEntry = (PluginEntry) Class.forName(entryClass).newInstance();

// 获取对应插件的配置文件
File configFile = new File(environment.getConfigDir(), pluginEntry.getName().toLowerCase() + ".conf");
// 加载与解析配置
PluginConfig pluginConfig = new PluginConfig(configFile, ConfigParser.parse(configFile));
// 对插件初始化
pluginEntry.init(environment, pluginConfig);

// 将插件添加到 Dispatcher 中
dispatcher.addTransformers(pluginEntry.getTransformers());

DebugInfo.debug("Plugin loaded: {name=" + pluginEntry.getName() + ", version=" + pluginEntry.getVersion() + ", author=" + pluginEntry.getAuthor() + "}");
} catch (Throwable e) {
DebugInfo.error("Parse plugin info failed", e);
}
}

执行插件

调用 Instrumentation#retransformClasses 后,会来到 Dispatcher#transform 方法,该方法会通过 classname 获取到该 class 所有的 MyTransformer,并循环遍历调用每个生命周期方法,最终得到 class 字节流进行返回即可。

要对 class 进行修改,需要借助 ASM,该库可以修改 class,并动态生成新的 class。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// com.janetfilter.core.Dispatcher#transform
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classFileBuffer) throws IllegalClassFormatException {
do {
if (null == className) {
break;
}

// 获取该 class 所有的 MyTransformer
List<MyTransformer> transformers = transformerMap.get(className);
// 未获取到,说明该 class 并不需要被 transformer
if (null == transformers) {
break;
}

int order = 0;

// 进行生命周期调用
try {
for (MyTransformer transformer : globalTransformers) {
transformer.before(className, classFileBuffer);
}

for (MyTransformer transformer : globalTransformers) {
classFileBuffer = transformer.preTransform(className, classFileBuffer, order++);
}

for (MyTransformer transformer : transformers) {
classFileBuffer = transformer.transform(className, classFileBuffer, order++);
}

for (MyTransformer transformer : globalTransformers) {
classFileBuffer = transformer.postTransform(className, classFileBuffer, order++);
}

for (MyTransformer transformer : globalTransformers) {
transformer.after(className, classFileBuffer);
}
} catch (Throwable e) {
DebugInfo.error("Transform class failed: " + className, e);
}
} while (false);

return classFileBuffer;
}

reference