diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..2bfa8b5 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +linux_header.sh text eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8f1859f --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +target* +*.dep +src/main/resources/git.properties +.flattened-*.xml + +.classpath +.factorypath +.project +.settings/ + +.vscode diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b531235 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8b92343 --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +![Logpresso Logo](logo.png) + +Logpresso FirewallOps is a single binary command-line tool for iptables and firewalld policy automation. It receives blocklist from Logpresso Watch and update firewall drop rule periodically. + +## Requirement +* [Logpresso Watch](https://logpresso.watch) service user +* iptables or firewalld installation + * iptables with ipset + * firewalld +* Connectivity with [Logpresso Watch](https://logpresso.watch) + * If you cannot connect to internet from server farm directly, use [Logpresso HTTP proxy](https://github.com/logpresso/http-proxy) for relay. + +### Usage +``` +Logpresso Firewall Ops 0.1.0 (2022-01-31) +Usage: logpresso-firewall-ops [start|install|uninstall] + start + install [api-key] [http-proxy ip:port] + uninstall +``` + +### Getting Started + +* Join Logpresso Watch and copy Blocklist API Key from Profile page. +* Install FirewallOps as systemd service. + * `# ./logpresso-firewall-ops install YOUR_BLOCKLIST_API_KEY` + * logpresso-firewall-ops.conf file will be created in the same directory which contains logpresso-firewall-ops binary. + * FirewallOps uses `firewalld-cmd --state` to detect firewalld is running. If firewalld is not available, it fallback to iptables backend. +* Review logpresso-firewall-ops.conf configuration. + * `[allowlist]` seciton contains default private network subnets to prevent accidental IP blocking like this: + ``` + [allowlist] + 10.0.0.0/8 + 172.16.0.0/12 + 192.168.0.0/16 + ``` + * Add more IP addresses related to normal service operation. +* Start systemd service. + * `# systemctl start logpresso-firewall-ops` +* Check service status + * For systemd service - `# systemctl status logpresso-firewall-ops` + * For iptables - `iptables -L -n` + * `DROP all -- 0.0.0.0/0 0.0.0.0/0 match-set logpresso-watch src` + * To see ipset content - `# ipset save logpresso-watch` + +### Uninstall +* Stop systemd service first. + * `# systemctl stop logpresso-firewall-ops` +* Run FirewallOps with uninstall option. + * '# ./logpresso-firewall-ops uninstall` + * It will delete systemd file, config file, and reload systemd daemon. + + +### Contact +If you have any question or issue, create an issue in this repository. + diff --git a/logo.png b/logo.png new file mode 100644 index 0000000..719a361 Binary files /dev/null and b/logo.png differ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..1a12124 --- /dev/null +++ b/pom.xml @@ -0,0 +1,120 @@ + + + 4.0.0 + com.logpresso + firewall-ops + 1.0.0 + jar + Logpresso Firewall Ops + + + + Apache Software License 2 + repo + + + + + UTF-8 + UTF-8 + 1.7 + 1.7 + + + + + + maven-jar-plugin + 3.2.0 + + + + com.logpresso.firewallops.FirewallOpsAgent + + + + + + org.codehaus.mojo + exec-maven-plugin + + + make-linux-launch-script + package + + java + + + com.logpresso.firewallops.AttachScriptAndJar + + + + + + attach_script_and_jar.base_dir + ${project.basedir} + + + attach_script_and_jar.target_dir + ${project.build.directory} + + + attach_script_and_jar.input_script + src/main/sh/linux_header.sh + + + attach_script_and_jar.input_jar + ${project.build.finalName}.jar + + + attach_script_and_jar.output_name + logpresso-firewall-ops + + + + + + + + + + + + native + + + + org.graalvm.buildtools + native-maven-plugin + 0.9.8 + true + + + build-native + + build + + package + + + + ${project.artifactId} + + + + + + + + + + junit + junit + 4.12 + test + + + diff --git a/src/main/java/com/logpresso/firewallops/AttachScriptAndJar.java b/src/main/java/com/logpresso/firewallops/AttachScriptAndJar.java new file mode 100644 index 0000000..881e30f --- /dev/null +++ b/src/main/java/com/logpresso/firewallops/AttachScriptAndJar.java @@ -0,0 +1,63 @@ +package com.logpresso.firewallops; + +import java.io.*; + +public class AttachScriptAndJar { + public static void main(String[] args) { + File scriptFile = new File( + new File(System.getProperty("attach_script_and_jar.base_dir")), + System.getProperty("attach_script_and_jar.input_script")); + + File jarFile = new File( + new File(System.getProperty("attach_script_and_jar.target_dir")), + System.getProperty("attach_script_and_jar.input_jar")); + + if (!scriptFile.exists()) + throw new IllegalArgumentException("input_script does not exists: " + scriptFile.getAbsolutePath()); + + if (!jarFile.exists()) + throw new IllegalArgumentException("input_jar does not exists: " + jarFile.getAbsolutePath()); + + File outputFile = new File( + new File(System.getProperty("attach_script_and_jar.target_dir")), + System.getProperty("attach_script_and_jar.output_name")); + + FileOutputStream fos = null; + FileInputStream fis1 = null, fis2 = null; + try { + System.out.println("input script: " + scriptFile.getAbsolutePath()); + System.out.println("input JAR : " + jarFile.getAbsolutePath()); + + fos = new FileOutputStream(outputFile, false); + fis1 = new FileInputStream(scriptFile); + fis2 = new FileInputStream(jarFile); + + byte[] buf = new byte[32768]; + for (int read = fis1.read(buf); read != -1; read = fis1.read(buf)) { + fos.write(buf, 0, read); + } + for (int read = fis2.read(buf); read != -1; read = fis2.read(buf)) { + fos.write(buf, 0, read); + } + + System.out.println("** Successfully merged script and jar: " + outputFile.getAbsolutePath()); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + close(fos); + close(fis1); + close(fis2); + } + } + + private static void close(Closeable c) { + if (c != null) { + try { + c.close(); + } catch (Exception ignored) { + } + } + } +} diff --git a/src/main/java/com/logpresso/firewallops/Backend.java b/src/main/java/com/logpresso/firewallops/Backend.java new file mode 100644 index 0000000..0ca7ae7 --- /dev/null +++ b/src/main/java/com/logpresso/firewallops/Backend.java @@ -0,0 +1,5 @@ +package com.logpresso.firewallops; + +public enum Backend { + FIREWALLD, IPTABLES +} diff --git a/src/main/java/com/logpresso/firewallops/Configuration.java b/src/main/java/com/logpresso/firewallops/Configuration.java new file mode 100644 index 0000000..9ed9095 --- /dev/null +++ b/src/main/java/com/logpresso/firewallops/Configuration.java @@ -0,0 +1,258 @@ +package com.logpresso.firewallops; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.regex.Pattern; + +import com.logpresso.firewallops.connector.FirewallConnector; +import com.logpresso.firewallops.connector.FirewalldConnector; +import com.logpresso.firewallops.connector.IptablesConnector; + +public class Configuration { + private UUID apiKey; + private boolean debug; + private Backend backend; + private Set allowlist = new HashSet(); + + public static void install(UUID apiKey, InetSocketAddress proxyAddr) { + Backend backend = installFirewallConnector(); + installConfigFile(backend, apiKey, proxyAddr); + installSystemdFile(); + } + + public static Configuration load() throws IOException { + Configuration c = new Configuration(); + Pattern regex = Pattern.compile("\\s+"); + File f = new File(IoUtils.getJarDir(), "logpresso-firewall-ops.conf"); + BufferedReader br = null; + + boolean allowlistSection = false; + try { + br = new BufferedReader(new InputStreamReader(new FileInputStream(f), "utf-8")); + while (true) { + String line = br.readLine(); + if (line == null) + break; + + line = line.trim(); + + if (line.isEmpty() || line.startsWith("#")) + continue; + + String[] tokens = regex.split(line); + String descriptor = tokens[0]; + + if (descriptor.equals("backend")) { + String value = getValue(tokens, "Specify backend value"); + try { + c.backend = Backend.valueOf(value.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Invalid backend type - " + value); + } + } else if (descriptor.equals("api-key")) { + String value = getValue(tokens, "Specify api-key value"); + try { + c.apiKey = UUID.fromString(value); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Invalid api-key format - " + value); + } + } else if (descriptor.equals("http-proxy")) { + try { + String s = getValue(tokens, "Specify http-proxy value (ip:port format)"); + int p = s.indexOf(':'); + if (p < 0) + throw new IllegalStateException("Missing http-proxy port - " + s); + + InetAddress host = InetAddress.getByName(s.substring(0, p)); + int port = Integer.parseInt(s.substring(p + 1)); + if (port < 0 || port > 65535) + throw new IllegalStateException("Invalid http-proxy port number range - " + tokens[1]); + + System.setProperty("https.proxyHost", host.getHostAddress()); + System.setProperty("https.proxyPort", Integer.toString(port)); + + } catch (NumberFormatException e) { + throw new IllegalStateException("Invalid http-proxy port number - " + tokens[1]); + } + } else if (descriptor.equals("loglevel")) { + c.debug = "debug".equals(getValue(tokens, "loglevel value is missing")); + } else if (descriptor.equals("[allowlist]")) { + allowlistSection = true; + } else { + if (allowlistSection) { + c.allowlist.add(tokens[0]); + } + } + } + + return c; + } finally { + if (br != null) + br.close(); + } + } + + private static String getValue(String[] tokens, String error) { + if (tokens.length < 2) + throw new IllegalStateException("port number is missing"); + + return tokens[1]; + } + + private static Backend installFirewallConnector() { + // check if firewalld is running + if (isFirewalldRunning()) { + new FirewalldConnector().install(); + return Backend.FIREWALLD; + } else { + new IptablesConnector().install(); + return Backend.IPTABLES; + } + } + + private static boolean isFirewalldRunning() { + try { + List output = PlatformUtils.execute("firewalld-cmd", "--state"); + return "running".equals(output.get(0)); + } catch (IOException e) { + return false; + } + } + + private static void installConfigFile(Backend backend, UUID apiKey, InetSocketAddress proxyAddr) { + File dir = IoUtils.getJarDir(); + File configFile = new File(dir, "logpresso-firewall-ops.conf"); + configFile.getParentFile().mkdirs(); + if (configFile.exists()) + throw new IllegalStateException("Cannot write file to " + configFile.getAbsolutePath()); + + BufferedWriter bw = null; + try { + bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(configFile), "utf-8")); + bw.write("# Logpresso Firewall Ops config file\n"); + bw.write("backend " + backend.name().toLowerCase() + "\n"); + bw.write("api-key " + apiKey + "\n"); + if (proxyAddr != null) + bw.write("http-proxy " + proxyAddr.getAddress().getHostAddress() + ":" + proxyAddr.getPort() + "\n"); + else + bw.write("# http-proxy x.x.x.x:8443\n"); + + bw.write("\n"); + bw.write("# Prevent accidental IP block\n"); + bw.write("# Network address/CIDR or IP address\n"); + bw.write("[allowlist]\n"); + bw.write("10.0.0.0/8\n"); + bw.write("172.16.0.0/12\n"); + bw.write("192.168.0.0/16\n"); + } catch (IOException e) { + throw new IllegalStateException("cannot write config file to " + configFile.getAbsolutePath(), e); + } finally { + if (bw != null) { + try { + bw.close(); + } catch (IOException e) { + } + } + } + + System.out.println("Wrote " + configFile.length() + " bytes to " + configFile.getAbsolutePath()); + } + + private static void installSystemdFile() { + File dir = IoUtils.getJarDir(); + File serviceFile = new File("/lib/systemd/system/logpresso-firewall-ops.service"); + BufferedWriter bw = null; + try { + bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(serviceFile), "utf-8")); + bw.write("[Unit]\n"); + bw.write("Description=Logpresso Firewall Ops\n"); + bw.write("After=multi-user.target network.target\n"); + bw.write("ConditionPathExists=" + dir.getAbsolutePath() + "/logpresso-firewall-ops.conf\n\n"); + bw.write("[Service]\n"); + bw.write("Type=simple\n"); + bw.write("ExecStart=" + dir.getAbsolutePath() + "/logpresso-firewall-ops start\n"); + bw.write("Restart=on-failure\n"); + bw.write("[Install]\n"); + bw.write("WantedBy=multi-user.target\n"); + } catch (IOException e) { + throw new IllegalStateException("cannot write systemd file to " + serviceFile.getAbsolutePath(), e); + } finally { + if (bw != null) { + try { + bw.close(); + } catch (IOException e) { + } + } + } + + System.out.println("Wrote " + serviceFile.length() + " bytes to " + serviceFile.getAbsolutePath()); + + try { + PlatformUtils.execute("systemctl", "daemon-reload"); + } catch (IOException e) { + } + } + + public static void uninstall() { + if (isFirewalldRunning()) { + new FirewalldConnector().uninstall(); + } else { + new IptablesConnector().uninstall(); + } + + File serviceFile = new File("/lib/systemd/system/logpresso-firewall-ops.service"); + if (!serviceFile.exists()) { + System.out.println("Error: service file not found"); + return; + } + + if (serviceFile.delete()) { + System.out.println("uninstalled systemd service"); + } else { + System.out.println("Cannot delete service file " + serviceFile.getAbsolutePath()); + } + + // delete config file + File dir = IoUtils.getJarDir(); + File configFile = new File(dir, "logpresso-firewall-ops.conf"); + configFile.delete(); + + try { + PlatformUtils.execute("systemctl", "daemon-reload"); + } catch (IOException e) { + } + } + + public FirewallConnector getConnector() { + if (backend == Backend.FIREWALLD) + return new FirewalldConnector(); + else if (backend == Backend.IPTABLES) + return new IptablesConnector(); + else + throw new UnsupportedOperationException(); + } + + public UUID getApiKey() { + return apiKey; + } + + public boolean isDebug() { + return debug; + } + + public Set getAllowlist() { + return allowlist; + } +} diff --git a/src/main/java/com/logpresso/firewallops/FirewallOpsAgent.java b/src/main/java/com/logpresso/firewallops/FirewallOpsAgent.java new file mode 100644 index 0000000..3ceeaa8 --- /dev/null +++ b/src/main/java/com/logpresso/firewallops/FirewallOpsAgent.java @@ -0,0 +1,185 @@ +package com.logpresso.firewallops; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class FirewallOpsAgent { + private Configuration conf; + + public static void main(String[] args) { + System.out.println("Logpresso Firewall Ops 1.0.0 (2022-02-03)"); + java.security.Security.setProperty("networkaddress.cache.ttl", "30"); + + if (args.length == 0) { + printUsage(); + return; + } + + String mode = args[0]; + + try { + if ("start".equals(mode)) { + Configuration c = Configuration.load(); + new FirewallOpsAgent().run(c); + } else if ("install".equals(mode)) { + if (args.length < 2) { + printUsage(); + return; + } + + UUID apiKey = UUID.fromString(args[1]); + InetSocketAddress proxyAddr = null; + if (args.length > 2) { + String proxy = args[2]; + int p = proxy.indexOf(':'); + if (p < 0) { + System.out.println("Error: missing proxy port"); + return; + } + + InetAddress host = InetAddress.getByName(proxy.substring(0, p)); + int port = Integer.parseInt(proxy.substring(p + 1)); + proxyAddr = new InetSocketAddress(host, port); + } + + Configuration.install(apiKey, proxyAddr); + } else if ("uninstall".equals(mode)) { + Configuration.uninstall(); + } + } catch (Throwable t) { + System.out.println("Error: " + t.getMessage()); + } + } + + private static void printUsage() { + System.out.println("Usage: logpresso-firewall-ops [start|install|uninstall]"); + System.out.println(" start"); + System.out.println(" install [api-key] [http-proxy ip:port]"); + System.out.println(" uninstall"); + } + + public void run(Configuration conf) { + this.conf = conf; + + int interval = 60000; + String lastTag = ""; + + int i = 0; + while (true) { + try { + if (i++ != 0) + Thread.sleep(interval); + + lastTag = downloadBlocklist(interval, lastTag); + + } catch (InterruptedException e) { + // ignore + } catch (Throwable t) { + System.out.println("Error: " + t.getMessage()); + } + } + } + + private String downloadBlocklist(int interval, String lastTag) { + System.out.println("Checking Logpresso Watch blocklist.."); + HttpURLConnection conn = null; + try { + UUID hostGuid = ensureHostGuid(); + String target = "https://watch.logpresso.com"; + conn = (HttpURLConnection) new URL(target + "/blocklist/policy?host_guid=" + hostGuid + "&tag=" + lastTag) + .openConnection(); + conn.setConnectTimeout(30000); + conn.setReadTimeout(30000); + conn.setRequestProperty("Authorization", "Bearer " + conf.getApiKey()); + + int status = conn.getResponseCode(); + if (status == 200) { + lastTag = updateBlocklist(conn.getInputStream()); + } else if (status == 304) { + System.out.println("Not modified"); + } else if (status == 503) { + System.out.println("Error: service unavailable"); + } else if (status == 401) { + System.out.println("Error: unauthorized api key"); + } else if (status == 404) { + System.out.println("Error: resource not found"); + } else { + System.out.println("Error: http error status " + status); + } + + } catch (IOException e) { + throw new IllegalStateException(e); + } finally { + if (conn != null) + conn.disconnect(); + } + + return lastTag; + } + + private String updateBlocklist(InputStream is) throws IOException { + String tag = null; + List blocklist = new ArrayList(); + BufferedReader br = null; + try { + br = new BufferedReader(new InputStreamReader(is, "utf-8")); + while (true) { + String line = br.readLine(); + if (line == null) + break; + + line = line.trim(); + if (line.isEmpty()) + continue; + + if (line.startsWith("# tag ")) { + tag = line.substring("# tag ".length()).trim(); + } else if (line.startsWith("#")) { + continue; + } else { + blocklist.add(InetAddress.getByName(line)); + } + } + + } finally { + IoUtils.ensureClose(br); + } + + System.out.println("Downloaded " + blocklist.size() + " items. new tag is " + tag); + conf.getConnector().deployBlocklist(blocklist); + + return tag; + + } + + private static UUID ensureHostGuid() { + File dir = IoUtils.getJarDir(); + File guidFile = new File(dir, "logpresso-firewall-ops.guid"); + if (guidFile.exists()) { + try { + return UUID.fromString(IoUtils.readLine(guidFile)); + } catch (IOException e) { + throw new IllegalStateException("Cannot read logpresso-firewall-ops.guid file", e); + } + } else { + try { + UUID newGuid = UUID.randomUUID(); + IoUtils.writeLine(guidFile, newGuid.toString()); + return newGuid; + } catch (IOException e) { + throw new IllegalStateException("Cannot write logpresso-firewall-ops.guid file", e); + } + } + } + +} diff --git a/src/main/java/com/logpresso/firewallops/IoUtils.java b/src/main/java/com/logpresso/firewallops/IoUtils.java new file mode 100644 index 0000000..cba6eaf --- /dev/null +++ b/src/main/java/com/logpresso/firewallops/IoUtils.java @@ -0,0 +1,84 @@ +package com.logpresso.firewallops; + +import java.io.BufferedReader; +import java.io.Closeable; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.List; + +public class IoUtils { + public static File getJarDir() { + try { + File jarPath = new File( + URLDecoder.decode(IoUtils.class.getProtectionDomain().getCodeSource().getLocation().getPath(), "utf-8")); + return jarPath.getParentFile(); + } catch (UnsupportedEncodingException e) { + // unreachable + throw new IllegalStateException(e); + } + } + + public static List loadLines(File f) throws IOException { + List lines = new ArrayList(); + FileInputStream fis = null; + BufferedReader br = null; + try { + br = new BufferedReader(new InputStreamReader(new FileInputStream(f), "utf-8")); + + while (true) { + String line = br.readLine(); + if (line == null) + break; + + line = line.trim(); + + if (line.startsWith("#") || line.isEmpty()) + continue; + + lines.add(line); + } + + return lines; + } finally { + IoUtils.ensureClose(fis); + IoUtils.ensureClose(br); + } + } + + public static String readLine(File f) throws IOException { + BufferedReader br = null; + FileInputStream fis = null; + try { + fis = new FileInputStream(f); + br = new BufferedReader(new InputStreamReader(fis, "utf-8")); + return br.readLine(); + } finally { + IoUtils.ensureClose(br); + } + } + + public static void writeLine(File f, String line) throws IOException { + FileOutputStream fos = null; + try { + fos = new FileOutputStream(f); + fos.write(line.getBytes("utf-8")); + } finally { + IoUtils.ensureClose(fos); + } + } + + public static void ensureClose(Closeable c) { + if (c != null) { + try { + c.close(); + } catch (Throwable t) { + } + } + } +} diff --git a/src/main/java/com/logpresso/firewallops/NetworkAddress.java b/src/main/java/com/logpresso/firewallops/NetworkAddress.java new file mode 100644 index 0000000..a844a8c --- /dev/null +++ b/src/main/java/com/logpresso/firewallops/NetworkAddress.java @@ -0,0 +1,66 @@ +package com.logpresso.firewallops; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +public class NetworkAddress { + private InetAddress addr; + private int cidr; + private long start; + private long end; + + public NetworkAddress(InetAddress addr, int cidr) { + long mask = getMask(cidr); + + this.start = toLong(addr) & mask; + this.end = (start | ~mask) & 0xffffffffL; + this.addr = toIp(start); + this.cidr = cidr; + } + + public InetAddress getStartIp() { + return toIp(start); + } + + public InetAddress getEndIp() { + return toIp(end); + } + + private static InetAddress toIp(long ip) { + byte b1 = (byte) ((ip >> 24) & 0xff); + byte b2 = (byte) ((ip >> 16) & 0xff); + byte b3 = (byte) ((ip >> 8) & 0xff); + byte b4 = (byte) (ip & 0xff); + + byte[] b = new byte[] { b1, b2, b3, b4 }; + try { + return InetAddress.getByAddress(b); + } catch (UnknownHostException e) { + throw new IllegalArgumentException("unreachable"); + } + } + + private static long getMask(int cidr) { + long mask = 0; + for (int i = 0; i < cidr; i++) + mask |= 1 << (31 - i); + + return mask; + } + + public boolean contains(InetAddress ip) { + long v = toLong(ip); + return start <= v && v <= end; + } + + private long toLong(InetAddress ip) { + byte[] b = ip.getAddress(); + return (((b[0] & 0xff) << 24) | ((b[1] & 0xff) << 16) | ((b[2] & 0xff) << 8) | (b[3] & 0xff)) & 0xffffffffL; + } + + @Override + public String toString() { + return String.format("%s/%d (%d~%d)", addr.getHostAddress(), cidr, start, end); + } + +} diff --git a/src/main/java/com/logpresso/firewallops/PlatformUtils.java b/src/main/java/com/logpresso/firewallops/PlatformUtils.java new file mode 100644 index 0000000..6f7a8ea --- /dev/null +++ b/src/main/java/com/logpresso/firewallops/PlatformUtils.java @@ -0,0 +1,136 @@ +package com.logpresso.firewallops; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +public class PlatformUtils { + public final static boolean isWindows; + public final static boolean isUnixLike; + public final static boolean isLinux; + public final static boolean isAIX; + public final static boolean isHPUX; + public final static boolean isSolaris; + public final static boolean isMacOS; + static { + String osname = System.getProperty("os.name").toLowerCase(); + isWindows = osname.startsWith("win"); + isLinux = osname.startsWith("linux"); + isAIX = osname.contains("aix"); + isHPUX = osname.contains("hpux") || osname.contains("hp-ux"); + isSolaris = osname.contains("solaris") || osname.contains("sunos"); + isMacOS = osname.contains("mac"); + isUnixLike = isLinux || isAIX || isHPUX || isSolaris || isMacOS; + if (!isWindows && !isUnixLike) + throw new UnsupportedOperationException(); + } + + public static String getHostname(boolean debug) { + // Try to fetch hostname without DNS resolving for closed network + boolean isWindows = File.separatorChar == '\\'; + if (isWindows) { + return System.getenv("COMPUTERNAME"); + } else { + Process p = null; + try { + p = Runtime.getRuntime().exec("uname -n"); + BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream())); + + String line = br.readLine(); + return (line == null) ? null : line.trim(); + } catch (IOException e) { + if (debug) + e.printStackTrace(); + + return null; + } finally { + if (p != null) + p.destroy(); + } + } + } + + public static String getHomeDir() { + if (isUnixLike) + return PlatformUtils.getenv("HOME"); + else + return PlatformUtils.getenv("USERPROFILE"); + } + + public static String getenv(String var) { + String val = System.getenv(var); + if (val == null || val.trim().isEmpty()) + return null; + else + return val; + } + + public static String resolvePath(String command) { + if (new File(command).isAbsolute()) + return command; + + String pathEnv = PlatformUtils.getenv("PATH"); + if (pathEnv == null || pathEnv.trim().isEmpty()) + pathEnv = ""; + + String[] paths = pathEnv.split(Pattern.quote(System.getProperty("path.separator"))); + for (String path : paths) { + File candidate = new File(path, command); + if (candidate.exists() && candidate.canExecute()) + return candidate.toString(); + } + + return command; + } + + public static List execute(String... commands) throws IOException { + List output = new ArrayList(); + Process p = null; + BufferedReader br = null; + try { + commands[0] = resolvePath(commands[0]); + ProcessBuilder pb = new ProcessBuilder(commands); + pb.redirectErrorStream(true); + p = pb.start(); + br = new BufferedReader(new InputStreamReader(p.getInputStream())); + while (true) { + String line = br.readLine(); + if (line == null) + break; + + output.add(line); + } + + return output; + } finally { + if (br != null) { + try { + br.close(); + } catch (Throwable t) { + } + } + + if (p != null) { + try { + p.waitFor(); + } catch (Throwable t) { + } + } + } + } + + public static boolean isRoot() throws IOException { + if (!isLinux) + return false; + + List output = execute("/usr/bin/id", "-u"); + if (output.isEmpty()) + return false; + + return output.get(0).equals("0"); + } +} \ No newline at end of file diff --git a/src/main/java/com/logpresso/firewallops/connector/FirewallConnector.java b/src/main/java/com/logpresso/firewallops/connector/FirewallConnector.java new file mode 100644 index 0000000..b985504 --- /dev/null +++ b/src/main/java/com/logpresso/firewallops/connector/FirewallConnector.java @@ -0,0 +1,14 @@ +package com.logpresso.firewallops.connector; + +import java.io.IOException; +import java.net.InetAddress; +import java.util.List; + +public interface FirewallConnector { + + void install() throws IOException; + + void uninstall() throws IOException; + + void deployBlocklist(List addresses) throws IOException; +} diff --git a/src/main/java/com/logpresso/firewallops/connector/FirewalldConnector.java b/src/main/java/com/logpresso/firewallops/connector/FirewalldConnector.java new file mode 100644 index 0000000..481b872 --- /dev/null +++ b/src/main/java/com/logpresso/firewallops/connector/FirewalldConnector.java @@ -0,0 +1,91 @@ +package com.logpresso.firewallops.connector; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.net.InetAddress; +import java.util.List; + +import com.logpresso.firewallops.IoUtils; +import com.logpresso.firewallops.PlatformUtils; + +public class FirewalldConnector implements FirewallConnector { + + private static final File configFile = new File("/etc/firewalld/ipsets/logpresso-watch.xml"); + private static final File backupFile = new File("/etc/firewalld/ipsets/logpresso-watch.xml.old"); + + public void install() { + try { + // create ipset + if (!configFile.exists()) { + List output = PlatformUtils.execute("firewall-cmd", "--permanent", "--new-ipset=logpresso-watch", + "--type=hash:ip"); + + if (!"success".equals(output.get(output.size() - 1))) + throw new IllegalStateException("Cannot create ipset - " + output.get(0)); + } + + PlatformUtils.execute("firewall-cmd", "--permanent", "--zone=drop", "--add-source=ipset:logpresso-watch"); + + reloadFirewalld(); + + System.out.println("Installed logpresso-watch ipset on firewalld."); + } catch (IOException e) { + throw new IllegalStateException(e.getMessage(), e); + } + } + + public void uninstall() { + if (!configFile.exists()) + throw new IllegalStateException("logpresso-firewall-ops.xml not found"); + + try { + // remove drop rule + List output = PlatformUtils.execute("firewall-cmd", "--permanent", "--zone=drop", + "--remove-source=ipset:logpresso-watch"); + if (!"success".equals(output.get(output.size() - 1))) + throw new IllegalStateException("Cannot delete drop rule - " + output.get(0)); + + // remove ipset + output = PlatformUtils.execute("firewall-cmd", "--permanent", "--delete-ipset=logpresso-watch"); + if (!"success".equals(output.get(output.size() - 1))) + throw new IllegalStateException("Cannot delete ipset - " + output.get(0)); + + reloadFirewalld(); + + System.out.println("Uninstalled logpresso-watch ipset from firewalld."); + } catch (IOException e) { + throw new IllegalStateException(e.getMessage(), e); + } + } + + private void reloadFirewalld() throws IOException { + List output = PlatformUtils.execute("firewall-cmd", "--reload"); + if (!"success".equals(output.get(output.size() - 1))) + throw new IllegalStateException("Cannot reload firewalld - " + output.get(0)); + } + + public void deployBlocklist(List addresses) { + backupFile.delete(); + configFile.renameTo(backupFile); + + BufferedWriter bw = null; + try { + bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(configFile), "utf-8")); + bw.write("\n"); + bw.write(""); + for (InetAddress addr : addresses) { + bw.write(" " + addr.getHostAddress() + ""); + } + bw.write(""); + + reloadFirewalld(); + } catch (IOException e) { + throw new IllegalStateException(e.getMessage(), e); + } finally { + IoUtils.ensureClose(bw); + } + } +} diff --git a/src/main/java/com/logpresso/firewallops/connector/IptablesConnector.java b/src/main/java/com/logpresso/firewallops/connector/IptablesConnector.java new file mode 100644 index 0000000..5aa0cb8 --- /dev/null +++ b/src/main/java/com/logpresso/firewallops/connector/IptablesConnector.java @@ -0,0 +1,89 @@ +package com.logpresso.firewallops.connector; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.List; + +import com.logpresso.firewallops.IoUtils; +import com.logpresso.firewallops.PlatformUtils; + +public class IptablesConnector implements FirewallConnector { + + @Override + public void install() { + try { + PlatformUtils.execute("ipset", "-N", "logpresso-watch", "iphash"); + PlatformUtils.execute("iptables", "-I", "INPUT", "1", "-m", "set", "--match-set", "logpresso-watch", "src", "-j", + "DROP"); + System.out.println("Installed logpresso-watch ipset and drop rule to iptables."); + } catch (IOException e) { + throw new IllegalStateException(e.getMessage(), e); + } + } + + @Override + public void uninstall() { + try { + PlatformUtils.execute("iptables", "-D", "INPUT", "1", "-m", "set", "--match-set", "logpresso-watch", "src"); + PlatformUtils.execute("ipset", "destroy", "logpresso-watch"); + + System.out.println("Uninstalled iptables drop rule and logpresso-watch ipset."); + } catch (IOException e) { + throw new IllegalStateException(e.getMessage(), e); + } + } + + @Override + public void deployBlocklist(List addresses) { + + List output = new ArrayList(); + Process p = null; + BufferedReader br = null; + BufferedWriter bw = null; + try { + PlatformUtils.execute("ipset", "flush", "logpresso-watch"); + + // write to stdin of ipset restore command + String[] commands = new String[] { "ipset", "restore", "-!" }; + + commands[0] = PlatformUtils.resolvePath(commands[0]); + ProcessBuilder pb = new ProcessBuilder(commands); + pb.redirectErrorStream(true); + p = pb.start(); + + bw = new BufferedWriter(new OutputStreamWriter(p.getOutputStream())); + bw.write("create logpresso-watch hash:ip family inet hashsize 1024 maxelem 65536\n"); + for (InetAddress addr : addresses) + bw.write("add logpresso-watch " + addr.getHostAddress() + "\n"); + + bw.close(); + + br = new BufferedReader(new InputStreamReader(p.getInputStream())); + while (true) { + String line = br.readLine(); + if (line == null) + break; + + output.add(line); + } + + } catch (IOException e) { + throw new IllegalStateException(e.getMessage(), e); + } finally { + IoUtils.ensureClose(bw); + IoUtils.ensureClose(br); + + if (p != null) { + try { + p.waitFor(); + } catch (Throwable t) { + } + } + } + } +} diff --git a/src/main/sh/linux_header.sh b/src/main/sh/linux_header.sh new file mode 100644 index 0000000..f46340d --- /dev/null +++ b/src/main/sh/linux_header.sh @@ -0,0 +1,82 @@ +#!/bin/sh +# +# chkconfig: 2345 99 20 +# +# ---------------------------------------------------------------------------- +# Copyright 2001-2006 The Apache Software Foundation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ---------------------------------------------------------------------------- +# +# Copyright (c) 2001-2006 The Apache Software Foundation. All rights +# reserved. + +# resolve links - $0 may be a softlink +PRG="$0" + +while [ -h "$PRG" ]; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`/"$link" + fi +done + +PRGDIR=`dirname "$PRG"` + +# If a specific java binary isn't specified search for the standard 'java' binary +if [ -z "$JAVA" ] ; then + # first, try built-in JRE + if [ -x "$PRGDIR/jre/bin/java" ] ; then + JAVA="$PRGDIR/jre/bin/java" + elif [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVA="$JAVA_HOME/jre/sh/java" + else + JAVA="$JAVA_HOME/bin/java" + fi + else + # or use PATH + JAVA=`command -v java` + fi +fi + +if [ ! -x "$JAVA" ] ; then + echo "Error: JAVA_HOME is not defined correctly." 1>&2 + echo " We cannot execute $JAVA" 1>&2 + exit 1 +fi + +exec $JAVA -jar $0 "$@" + +#### jar will be attached after following blank lines +#### following blank lines are intentional. + + + + + + + + + + + + + + + + diff --git a/src/test/java/com/logpresso/firewallops/NetworkAddressTest.java b/src/test/java/com/logpresso/firewallops/NetworkAddressTest.java new file mode 100644 index 0000000..acc121a --- /dev/null +++ b/src/test/java/com/logpresso/firewallops/NetworkAddressTest.java @@ -0,0 +1,36 @@ +package com.logpresso.firewallops; + +import static org.junit.Assert.assertEquals; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +import org.junit.Test; + +public class NetworkAddressTest { + @Test + public void testCidr() { + + NetworkAddress private8 = new NetworkAddress(ip("10.0.0.0"), 8); + NetworkAddress private12 = new NetworkAddress(ip("172.16.0.0"), 12); + NetworkAddress private16 = new NetworkAddress(ip("192.168.0.0"), 16); + + assertEquals(ip("10.0.0.0"), private8.getStartIp()); + assertEquals(ip("10.255.255.255"), private8.getEndIp()); + + assertEquals(ip("172.16.0.0"), private12.getStartIp()); + assertEquals(ip("172.31.255.255"), private12.getEndIp()); + + assertEquals(ip("192.168.0.0"), private16.getStartIp()); + assertEquals(ip("192.168.255.255"), private16.getEndIp()); + + } + + private InetAddress ip(String s) { + try { + return InetAddress.getByName(s); + } catch (UnknownHostException e) { + throw new IllegalArgumentException(s); + } + } +}