add plugin system

Signed-off-by: pengzhile <pengzhile@gmail.com>
This commit is contained in:
pengzhile 2021-11-30 17:06:48 +08:00
parent 3fc69ee29f
commit 5586939a8f
15 changed files with 299 additions and 91 deletions

View File

@ -1,4 +1,4 @@
# ja-netfilter
# ja-netfilter v1.1.0
### A javaagent lib for network filter
@ -6,25 +6,19 @@
* download from the [releases page](https://github.com/pengzhile/ja-netfilter/releases)
* add `-javaagent:/absolute/path/to/ja-netfilter.jar` argument (**Change to your actual path**)
* add as an argument of the `java` command.
eg: `java -javaagent:/absolute/path/to/ja-netfilter.jar -jar executable_jar_file.jar`
* add as an argument of the `java` command. eg: `java -javaagent:/absolute/path/to/ja-netfilter.jar -jar executable_jar_file.jar`
* some apps support the `JVM Options file`, you can add as a line of the `JVM Options file`.
* **WARNING: DO NOT put some unnecessary whitespace characters!**
* edit your own rule list config file. The `ja-netfilter` will look for it in the following order(find one and stop
searching):
* passed as args of `-javaagent`.
eg: `-javaagent:/absolute/path/to/ja-netfilter.jar=/home/neo/downloads/janf_config.txt`
* edit your own rule list config file. The `ja-netfilter` will look for it in the following order(find one and stop searching):
* passed as args of `-javaagent`. eg: `-javaagent:/absolute/path/to/ja-netfilter.jar=/home/neo/downloads/janf_config.txt`
* file path in environment variable: `JANF_CONFIG`
* file path in `java` startup property: `janf.config`
. `eg: java -Djanf.config="/home/neo/downloads/janf_config.txt"`
* some apps support the `JVM Options file`, you can add as a line of the `JVM Options file`
. `eg: -Djanf.config="/home/neo/downloads/janf_config.txt"`
* file path in the same dir as the `ja-netfilter.jar` (**PREFERRED!**)
* file path in `java` startup property: `janf.config`. `eg: java -Djanf.config="/home/neo/downloads/janf_config.txt"`
* some apps support the `JVM Options file`, you can add as a line of the `JVM Options file`. `eg: -Djanf.config="/home/neo/downloads/janf_config.txt"`
* file path in the same dir as the `ja-netfilter.jar`, no need for additional configuration (<font color=green>**PREFERRED!**</font>)
* file path in your home directory, named: `.janf_config.txt`. `eg: /home/neo/.janf_config.txt`
* file path in the subdirectory named `.config` in your home directory. `eg: /home/neo/.config/janf_config.txt`
* file path in the subdirectory named `.local/etc` in your home
directory. `eg: /home/neo/.local/ect/janf_config.txt`
* file path in the subdirectory named `.local/etc` in your home directory. `eg: /home/neo/.local/ect/janf_config.txt`
* file path in the directory named `/usr/local/etc`. `eg: /usr/local/etc/janf_config.txt`
* file path in the directory named `/etc`. eg: `/etc/janf_config.txt`
@ -33,6 +27,9 @@
## Config file format
```
[ABC]
# for the specified plugin called "ABC"
[URL]
EQUAL,https://someurl
@ -50,4 +47,17 @@ EQUAL,somedomain
* the `ja-netfilter` will **NOT** output debugging information by default
* add environment variable `JANF_DEBUG=1` and start to enable it
* or add system property `-Djanf.debug=1` to enable it
* or add system property `-Djanf.debug=1` to enable it
## Plugin system
* for developer:
* view the [scaffold project](!https://github.com/pengzhile/ja-netfilter-sample-plugin) written for the plug-in system
* compile your plugin and publish it
* just use your imagination~
* for user:
* download the jar file of the plugin
* put it in the subdirectory called `plugins` where the ja-netfilter.jar file is located
* enjoy the new capabilities brought by the plugin

View File

@ -6,7 +6,7 @@
<groupId>io.zhile.research</groupId>
<artifactId>ja-netfilter</artifactId>
<version>1.0.0</version>
<version>1.1.0</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

View File

@ -0,0 +1,64 @@
package io.zhile.research.ja.netfilter;
import io.zhile.research.ja.netfilter.commons.DebugInfo;
import io.zhile.research.ja.netfilter.transformers.MyTransformer;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import java.util.*;
public class Dispatcher implements ClassFileTransformer {
private static Dispatcher INSTANCE;
private final Map<String, List<MyTransformer>> transformerMap = new HashMap<>();
public static synchronized Dispatcher getInstance() {
if (null == INSTANCE) {
INSTANCE = new Dispatcher();
}
return INSTANCE;
}
public void addTransformer(MyTransformer transformer) {
List<MyTransformer> transformers = transformerMap.computeIfAbsent(transformer.getHookClassName(), k -> new ArrayList<>());
transformers.add(transformer);
}
public void addTransformers(List<MyTransformer> transformers) {
for (MyTransformer transformer : transformers) {
addTransformer(transformer);
}
}
public void addTransformers(MyTransformer[] transformers) {
addTransformers(Arrays.asList(transformers));
}
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classFileBuffer) throws IllegalClassFormatException {
do {
if (null == className) {
break;
}
List<MyTransformer> transformers = transformerMap.get(className);
if (null == transformers) {
break;
}
int order = 0;
try {
for (MyTransformer transformer : transformers) {
classFileBuffer = transformer.transform(className, classFileBuffer, order++);
}
} catch (Exception e) {
DebugInfo.output("Transform class failed: " + e.getMessage());
}
} while (false);
return classFileBuffer;
}
}

View File

@ -0,0 +1,40 @@
package io.zhile.research.ja.netfilter;
import io.zhile.research.ja.netfilter.commons.ConfigDetector;
import io.zhile.research.ja.netfilter.commons.ConfigParser;
import io.zhile.research.ja.netfilter.commons.DebugInfo;
import io.zhile.research.ja.netfilter.models.FilterConfig;
import io.zhile.research.ja.netfilter.plugin.PluginManager;
import java.io.File;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
public class Initializer {
public static void init(String args, Instrumentation inst, File currentDirectory) {
File configFile = ConfigDetector.detect(currentDirectory, args);
if (null == configFile) {
DebugInfo.output("Could not find any configuration files.");
} else {
DebugInfo.output("Current config file: " + configFile.getPath());
}
try {
FilterConfig.setCurrent(new FilterConfig(ConfigParser.parse(configFile)));
} catch (Exception e) {
DebugInfo.output(e.getMessage());
}
PluginManager.getInstance().loadPlugins(inst, currentDirectory);
for (Class<?> c : inst.getAllLoadedClasses()) {
try {
inst.retransformClasses(c);
} catch (UnmodifiableClassException e) {
// ok, ok. just ignore
}
}
inst.addTransformer(Dispatcher.getInstance(), true);
}
}

View File

@ -1,13 +1,9 @@
package io.zhile.research.ja.netfilter;
import io.zhile.research.ja.netfilter.commons.ConfigDetector;
import io.zhile.research.ja.netfilter.commons.ConfigParser;
import io.zhile.research.ja.netfilter.commons.DebugInfo;
import io.zhile.research.ja.netfilter.models.FilterConfig;
import java.io.File;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.net.URI;
import java.net.URL;
import java.util.jar.JarFile;
@ -28,35 +24,16 @@ public class Launcher {
return;
}
File currentFile = new File(jarURI.getPath());
File currentDirectory = currentFile.getParentFile();
try {
inst.appendToBootstrapClassLoaderSearch(new JarFile(jarURI.getPath()));
inst.appendToBootstrapClassLoaderSearch(new JarFile(currentFile));
} catch (Throwable e) {
DebugInfo.output("ERROR: Can not access ja-netfilter jar file.");
return;
}
File configFile = ConfigDetector.detect(new File(jarURI.getPath()).getParentFile().getPath(), args);
if (null == configFile) {
DebugInfo.output("Could not find any configuration files.");
} else {
DebugInfo.output("Current config file: " + configFile.getPath());
}
try {
FilterConfig.setCurrent(new FilterConfig(ConfigParser.parse(configFile)));
} catch (Exception e) {
DebugInfo.output(e.getMessage());
}
for (Class<?> c : inst.getAllLoadedClasses()) {
try {
inst.retransformClasses(c);
} catch (UnmodifiableClassException e) {
// ok, ok. just ignore
}
}
inst.addTransformer(new TransformDispatcher(), true);
Initializer.init(args, inst, currentDirectory); // for some custom UrlLoaders
}
private static void printUsage() {

View File

@ -1,43 +0,0 @@
package io.zhile.research.ja.netfilter;
import io.zhile.research.ja.netfilter.commons.DebugInfo;
import io.zhile.research.ja.netfilter.transformers.HttpClientTransformer;
import io.zhile.research.ja.netfilter.transformers.InetAddressTransformer;
import io.zhile.research.ja.netfilter.transformers.MyTransformer;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import java.util.HashMap;
import java.util.Map;
public class TransformDispatcher implements ClassFileTransformer {
public static final Map<String, MyTransformer> TRANSFORMER_MAP;
static {
TRANSFORMER_MAP = new HashMap<>();
TRANSFORMER_MAP.put("sun/net/www/http/HttpClient", new HttpClientTransformer());
TRANSFORMER_MAP.put("java/net/InetAddress", new InetAddressTransformer());
}
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classFileBuffer) throws IllegalClassFormatException {
do {
if (null == className) {
break;
}
MyTransformer transformer = TRANSFORMER_MAP.get(className);
if (null == transformer) {
break;
}
try {
return transformer.transform(className, classFileBuffer);
} catch (Exception e) {
DebugInfo.output("Transform class failed: " + e.getMessage());
}
} while (false);
return classFileBuffer;
}
}

View File

@ -7,6 +7,10 @@ import java.io.File;
public class ConfigDetector {
private static final String CONFIG_FILENAME = "janf_config.txt";
public static File detect(File currentDirectory, String args) {
return detect(currentDirectory.getPath(), args);
}
public static File detect(String currentDirectory, String args) {
File configFile = tryFile(args); // by javaagent argument

View File

@ -9,7 +9,7 @@ import java.net.SocketTimeoutException;
import java.net.URL;
public class URLFilter {
public static final String SECTION_NAME = "URL";
private static final String SECTION_NAME = "URL";
public static URL testURL(URL url) throws IOException {
if (null == url) {

View File

@ -60,4 +60,4 @@ public class FilterRule {
", rule='" + rule + '\'' +
'}';
}
}
}

View File

@ -0,0 +1,44 @@
package io.zhile.research.ja.netfilter.plugin;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;
public class PluginClassLoader extends ClassLoader {
private final JarFile jarFile;
public PluginClassLoader(JarFile jarFile) {
this.jarFile = jarFile;
}
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
byte[] bytes = loadClassFromFile(name);
return defineClass(name, bytes, 0, bytes.length);
}
private byte[] loadClassFromFile(String fileName) throws ClassNotFoundException {
String classFile = fileName.replace('.', '/') + ".class";
ZipEntry entry = jarFile.getEntry(classFile);
if (null == entry) {
throw new ClassNotFoundException("Class not found: " + fileName);
}
int length;
byte[] buffer = new byte[1024];
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
try (InputStream is = jarFile.getInputStream(entry)) {
while (-1 != (length = is.read(buffer))) {
byteStream.write(buffer, 0, length);
}
} catch (IOException e) {
throw new ClassNotFoundException("Can't access class: " + fileName, e);
}
return byteStream.toByteArray();
}
}

View File

@ -0,0 +1,24 @@
package io.zhile.research.ja.netfilter.plugin;
import io.zhile.research.ja.netfilter.models.FilterRule;
import io.zhile.research.ja.netfilter.transformers.MyTransformer;
import java.util.List;
public interface PluginEntry {
default void init(List<FilterRule> filterRules) {
// get plugin config
}
String getName();
default String getVersion() {
return "v1.0.0";
}
default String getDescription() {
return "A ja-netfilter plugin.";
}
List<MyTransformer> getTransformers();
}

View File

@ -0,0 +1,74 @@
package io.zhile.research.ja.netfilter.plugin;
import io.zhile.research.ja.netfilter.Dispatcher;
import io.zhile.research.ja.netfilter.commons.DebugInfo;
import io.zhile.research.ja.netfilter.models.FilterConfig;
import io.zhile.research.ja.netfilter.transformers.HttpClientTransformer;
import io.zhile.research.ja.netfilter.transformers.InetAddressTransformer;
import io.zhile.research.ja.netfilter.transformers.MyTransformer;
import io.zhile.research.ja.netfilter.utils.StringUtils;
import java.io.File;
import java.lang.instrument.Instrumentation;
import java.util.Arrays;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
public class PluginManager {
private static final String PLUGINS_DIR = "plugins";
private static final String ENTRY_NAME = "JANF-Plugin-Entry";
private static PluginManager INSTANCE;
public static synchronized PluginManager getInstance() {
if (null == INSTANCE) {
INSTANCE = new PluginManager();
}
return INSTANCE;
}
public void loadPlugins(Instrumentation inst, File currentDirectory) {
File pluginsDirectory = new File(currentDirectory, PLUGINS_DIR);
if (!pluginsDirectory.exists() || !pluginsDirectory.isDirectory()) {
return;
}
File[] pluginFiles = pluginsDirectory.listFiles((d, n) -> n.endsWith(".jar"));
if (null == pluginFiles) {
return;
}
Dispatcher.getInstance().addTransformers(new MyTransformer[]{ // built-in transformers
new HttpClientTransformer(),
new InetAddressTransformer()
});
for (File pluginFile : pluginFiles) {
try {
JarFile jarFile = new JarFile(pluginFile);
Manifest manifest = jarFile.getManifest();
String entryClass = manifest.getMainAttributes().getValue(ENTRY_NAME);
if (StringUtils.isEmpty(entryClass)) {
continue;
}
PluginClassLoader classLoader = new PluginClassLoader(jarFile);
Class<?> klass = Class.forName(entryClass, false, classLoader);
if (!Arrays.asList(klass.getInterfaces()).contains(PluginEntry.class)) {
continue;
}
inst.appendToBootstrapClassLoaderSearch(jarFile);
PluginEntry pluginEntry = (PluginEntry) Class.forName(entryClass).newInstance();
pluginEntry.init(FilterConfig.getBySection(pluginEntry.getName()));
Dispatcher.getInstance().addTransformers(pluginEntry.getTransformers());
DebugInfo.output("Plugin loaded: {name=" + pluginEntry.getName() + ", version=" + pluginEntry.getVersion() + "}");
} catch (Exception e) {
DebugInfo.output("Load plugin failed: " + e.getMessage());
}
}
}
}

View File

@ -8,7 +8,12 @@ import static jdk.internal.org.objectweb.asm.Opcodes.*;
public class HttpClientTransformer implements MyTransformer {
@Override
public byte[] transform(String className, byte[] classBytes) throws Exception {
public String getHookClassName() {
return "sun/net/www/http/HttpClient";
}
@Override
public byte[] transform(String className, byte[] classBytes, int order) throws Exception {
ClassReader reader = new ClassReader(classBytes);
ClassNode node = new ClassNode(ASM5);
reader.accept(node, 0);

View File

@ -8,7 +8,12 @@ import static jdk.internal.org.objectweb.asm.Opcodes.*;
public class InetAddressTransformer implements MyTransformer {
@Override
public byte[] transform(String className, byte[] classBytes) throws Exception {
public String getHookClassName() {
return "java/net/InetAddress";
}
@Override
public byte[] transform(String className, byte[] classBytes, int order) throws Exception {
ClassReader reader = new ClassReader(classBytes);
ClassNode node = new ClassNode(ASM5);
reader.accept(node, 0);

View File

@ -1,5 +1,9 @@
package io.zhile.research.ja.netfilter.transformers;
public interface MyTransformer {
byte[] transform(String className, byte[] classBytes) throws Exception;
String getHookClassName();
default byte[] transform(String className, byte[] classBytes, int order) throws Exception {
return classBytes;
}
}