mirror of
https://gitee.com/ja-netfilter/ja-netfilter.git
synced 2024-12-22 08:59:25 +08:00
add plugin system
Signed-off-by: pengzhile <pengzhile@gmail.com>
This commit is contained in:
parent
3fc69ee29f
commit
5586939a8f
40
README.md
40
README.md
@ -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
|
||||
|
2
pom.xml
2
pom.xml
@ -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>
|
||||
|
64
src/main/java/io/zhile/research/ja/netfilter/Dispatcher.java
Normal file
64
src/main/java/io/zhile/research/ja/netfilter/Dispatcher.java
Normal 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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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() {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -60,4 +60,4 @@ public class FilterRule {
|
||||
", rule='" + rule + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user