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; + } }