diff --git a/README.md b/README.md
index bd699e6..8e1735f 100644
--- a/README.md
+++ b/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 (**PREFERRED!**)
* 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
\ No newline at end of file
+* 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
+
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index f5c6388..d34ede2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
io.zhile.research
ja-netfilter
- 1.0.0
+ 1.1.0
UTF-8
diff --git a/src/main/java/io/zhile/research/ja/netfilter/Dispatcher.java b/src/main/java/io/zhile/research/ja/netfilter/Dispatcher.java
new file mode 100644
index 0000000..5fbee17
--- /dev/null
+++ b/src/main/java/io/zhile/research/ja/netfilter/Dispatcher.java
@@ -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> transformerMap = new HashMap<>();
+
+ public static synchronized Dispatcher getInstance() {
+ if (null == INSTANCE) {
+ INSTANCE = new Dispatcher();
+ }
+
+ return INSTANCE;
+ }
+
+ public void addTransformer(MyTransformer transformer) {
+ List transformers = transformerMap.computeIfAbsent(transformer.getHookClassName(), k -> new ArrayList<>());
+
+ transformers.add(transformer);
+ }
+
+ public void addTransformers(List 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 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;
+ }
+}
diff --git a/src/main/java/io/zhile/research/ja/netfilter/Initializer.java b/src/main/java/io/zhile/research/ja/netfilter/Initializer.java
new file mode 100644
index 0000000..f96250e
--- /dev/null
+++ b/src/main/java/io/zhile/research/ja/netfilter/Initializer.java
@@ -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);
+ }
+}
diff --git a/src/main/java/io/zhile/research/ja/netfilter/Launcher.java b/src/main/java/io/zhile/research/ja/netfilter/Launcher.java
index 6d6e23f..9fb5de3 100644
--- a/src/main/java/io/zhile/research/ja/netfilter/Launcher.java
+++ b/src/main/java/io/zhile/research/ja/netfilter/Launcher.java
@@ -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() {
diff --git a/src/main/java/io/zhile/research/ja/netfilter/TransformDispatcher.java b/src/main/java/io/zhile/research/ja/netfilter/TransformDispatcher.java
deleted file mode 100644
index d27ab65..0000000
--- a/src/main/java/io/zhile/research/ja/netfilter/TransformDispatcher.java
+++ /dev/null
@@ -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 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;
- }
-}
diff --git a/src/main/java/io/zhile/research/ja/netfilter/commons/ConfigDetector.java b/src/main/java/io/zhile/research/ja/netfilter/commons/ConfigDetector.java
index 3fd2ae2..ca03d1c 100644
--- a/src/main/java/io/zhile/research/ja/netfilter/commons/ConfigDetector.java
+++ b/src/main/java/io/zhile/research/ja/netfilter/commons/ConfigDetector.java
@@ -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
diff --git a/src/main/java/io/zhile/research/ja/netfilter/filters/URLFilter.java b/src/main/java/io/zhile/research/ja/netfilter/filters/URLFilter.java
index 1fdf48c..68ac5cd 100644
--- a/src/main/java/io/zhile/research/ja/netfilter/filters/URLFilter.java
+++ b/src/main/java/io/zhile/research/ja/netfilter/filters/URLFilter.java
@@ -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) {
diff --git a/src/main/java/io/zhile/research/ja/netfilter/models/FilterRule.java b/src/main/java/io/zhile/research/ja/netfilter/models/FilterRule.java
index b4c7a65..8f099c9 100644
--- a/src/main/java/io/zhile/research/ja/netfilter/models/FilterRule.java
+++ b/src/main/java/io/zhile/research/ja/netfilter/models/FilterRule.java
@@ -60,4 +60,4 @@ public class FilterRule {
", rule='" + rule + '\'' +
'}';
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/io/zhile/research/ja/netfilter/plugin/PluginClassLoader.java b/src/main/java/io/zhile/research/ja/netfilter/plugin/PluginClassLoader.java
new file mode 100644
index 0000000..6e0e967
--- /dev/null
+++ b/src/main/java/io/zhile/research/ja/netfilter/plugin/PluginClassLoader.java
@@ -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();
+ }
+}
diff --git a/src/main/java/io/zhile/research/ja/netfilter/plugin/PluginEntry.java b/src/main/java/io/zhile/research/ja/netfilter/plugin/PluginEntry.java
new file mode 100644
index 0000000..1c9bf75
--- /dev/null
+++ b/src/main/java/io/zhile/research/ja/netfilter/plugin/PluginEntry.java
@@ -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 filterRules) {
+ // get plugin config
+ }
+
+ String getName();
+
+ default String getVersion() {
+ return "v1.0.0";
+ }
+
+ default String getDescription() {
+ return "A ja-netfilter plugin.";
+ }
+
+ List getTransformers();
+}
diff --git a/src/main/java/io/zhile/research/ja/netfilter/plugin/PluginManager.java b/src/main/java/io/zhile/research/ja/netfilter/plugin/PluginManager.java
new file mode 100644
index 0000000..b08a60c
--- /dev/null
+++ b/src/main/java/io/zhile/research/ja/netfilter/plugin/PluginManager.java
@@ -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());
+ }
+ }
+ }
+}
diff --git a/src/main/java/io/zhile/research/ja/netfilter/transformers/HttpClientTransformer.java b/src/main/java/io/zhile/research/ja/netfilter/transformers/HttpClientTransformer.java
index bcd094f..60c4303 100644
--- a/src/main/java/io/zhile/research/ja/netfilter/transformers/HttpClientTransformer.java
+++ b/src/main/java/io/zhile/research/ja/netfilter/transformers/HttpClientTransformer.java
@@ -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);
diff --git a/src/main/java/io/zhile/research/ja/netfilter/transformers/InetAddressTransformer.java b/src/main/java/io/zhile/research/ja/netfilter/transformers/InetAddressTransformer.java
index c055eda..f95410b 100644
--- a/src/main/java/io/zhile/research/ja/netfilter/transformers/InetAddressTransformer.java
+++ b/src/main/java/io/zhile/research/ja/netfilter/transformers/InetAddressTransformer.java
@@ -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);
diff --git a/src/main/java/io/zhile/research/ja/netfilter/transformers/MyTransformer.java b/src/main/java/io/zhile/research/ja/netfilter/transformers/MyTransformer.java
index 1543cb1..5b2d6f8 100644
--- a/src/main/java/io/zhile/research/ja/netfilter/transformers/MyTransformer.java
+++ b/src/main/java/io/zhile/research/ja/netfilter/transformers/MyTransformer.java
@@ -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;
+ }
}