Skip to content

Commit

Permalink
Rewrite network ACLs
Browse files Browse the repository at this point in the history
* A cleaner format;
* Provides better behavior when importing a non-compatible ACL;
* Supports importing URL from a imported file with up to 10 layers.
  • Loading branch information
Mygod committed Sep 4, 2017
1 parent d7183fe commit b8d7ade
Show file tree
Hide file tree
Showing 7 changed files with 214 additions and 142 deletions.
1 change: 1 addition & 0 deletions mobile/src/main/res/values/arrays.xml
Original file line number Diff line number Diff line change
Expand Up @@ -224,5 +224,6 @@
<string-array name="acl_rule_templates">
<item>@string/acl_rule_templates_generic</item>
<item>@string/acl_rule_templates_domain</item>
<item>URL</item>
</string-array>
</resources>
2 changes: 1 addition & 1 deletion mobile/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@
<string name="custom_rules">Custom rules</string>
<string name="action_selection">Selection…</string>
<string name="action_add_rule">Add rule(s)…</string>
<string name="acl_rule_templates_generic">URL, Subnet or Hostname PCRE pattern</string>
<string name="acl_rule_templates_generic">Subnet or Hostname PCRE pattern</string>
<string name="acl_rule_templates_domain">Domain name and all its subdomain names</string>
<string name="edit_rule">Edit rule</string>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,7 @@ trait BaseService extends Service {
profile.method = proxy(3).trim
}

if (profile.route == Acl.CUSTOM_RULES) // rationalize custom rules
Acl.save(Acl.CUSTOM_RULES, new Acl().fromId(Acl.CUSTOM_RULES), true)
if (profile.route == Acl.CUSTOM_RULES) Acl.save(Acl.CUSTOM_RULES_FLATTENED, Acl.customRules.flatten(10))

plugin = new PluginConfiguration(profile.plugin).selectedOptions
pluginPath = PluginManager.init(plugin)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,10 @@ class ShadowsocksNatService extends BaseService {

if (profile.route != Acl.ALL) {
cmd += "--acl"
cmd += Acl.getFile(profile.route).getAbsolutePath
cmd += Acl.getFile(profile.route match {
case Acl.CUSTOM_RULES => Acl.CUSTOM_RULES_FLATTENED
case route => route
}).getAbsolutePath
}

sslocalProcess = new GuardedProcess(cmd: _*).start()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,10 @@ class ShadowsocksVpnService extends VpnService with BaseService {

if (profile.route != Acl.ALL) {
cmd += "--acl"
cmd += Acl.getFile(profile.route).getAbsolutePath
cmd += Acl.getFile(profile.route match {
case Acl.CUSTOM_RULES => Acl.CUSTOM_RULES_FLATTENED
case route => route
}).getAbsolutePath
}

if (TcpFastOpen.sendEnabled) cmd += "--fast-open"
Expand Down
153 changes: 94 additions & 59 deletions mobile/src/main/scala/com/github/shadowsocks/acl/Acl.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.github.shadowsocks.acl

import java.io.{File, FileNotFoundException, IOException}
import java.io.{File, FileNotFoundException}
import java.net.URL

import android.util.Log
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.utils.IOUtils
import com.j256.ormlite.field.DatabaseField
Expand All @@ -17,100 +19,128 @@ import scala.io.Source
* @author Mygod
*/
class Acl {
import Acl._

@DatabaseField(generatedId = true)
var id: Int = _

val hostnames = new mutable.SortedList[String]()
val bypassHostnames = new mutable.SortedList[String]()
val proxyHostnames = new mutable.SortedList[String]()
val subnets = new mutable.SortedList[Subnet]()
val urls = new mutable.SortedList[String]()
val urls = new mutable.SortedList[URL]()(urlOrdering)

@DatabaseField
var bypass: Boolean = _

def isUrl(url: String): Boolean = url.startsWith("http://") || url.startsWith("https://")
def getBypassHostnamesString: String = bypassHostnames.mkString("\n")
def getProxyHostnamesString: String = proxyHostnames.mkString("\n")
def getSubnetsString: String = subnets.mkString("\n")
def getUrlsString: String = urls.mkString("\n")
def setBypassHostnamesString(value: String) {
bypassHostnames.clear()
bypassHostnames ++= value.split("\n")
}
def setProxyHostnamesString(value: String) {
proxyHostnames.clear()
proxyHostnames ++= value.split("\n")
}
def setSubnetsString(value: String) {
subnets.clear()
subnets ++= value.split("\n").map(Subnet.fromString)
}
def setUrlsString(value: String) {
urls.clear()
urls ++= value.split("\n").map(new URL(_))
}

def fromAcl(other: Acl): Acl = {
hostnames.clear()
hostnames ++= other.hostnames
bypassHostnames.clear()
bypassHostnames ++= other.bypassHostnames
proxyHostnames.clear()
proxyHostnames ++= other.proxyHostnames
subnets.clear()
subnets ++= other.subnets
urls.clear()
urls ++= other.urls
bypass = other.bypass
this
}

def fromSource(value: Source, defaultBypass: Boolean = true): Acl = {
subnets.clear()
def fromSource(value: Source, defaultBypass: Boolean = false): Acl = {
bypassHostnames.clear()
proxyHostnames.clear()
this.subnets.clear()
urls.clear()
bypass = defaultBypass
var in_urls = false
for (line <- value.getLines()) (line.trim.indexOf('#') match {
case 0 => {
line.indexOf("NETWORK_ACL_BEGIN") match {
case -1 =>
case index => in_urls = true
}
line.indexOf("NETWORK_ACL_END") match {
case -1 =>
case index => in_urls = false
}
"" // ignore any comment lines
}
case index => if (!in_urls) line else ""
lazy val bypassSubnets = new mutable.SortedList[Subnet]()
lazy val proxySubnets = new mutable.SortedList[Subnet]()
var hostnames: mutable.SortedList[String] = if (defaultBypass) proxyHostnames else bypassHostnames
var subnets: mutable.SortedList[Subnet] = if (defaultBypass) proxySubnets else bypassSubnets
for (line <- value.getLines()) (line.indexOf('#') match {
case -1 => line
case index =>
line.substring(index + 1) match {
case networkAclParser(url) => urls.add(new URL(url))
case _ => // ignore
}
line.substring(0, index) // trim comments
}).trim match {
// Ignore all section controls
case "[outbound_block_list]" =>
hostnames = null
subnets = null
case "[black_list]" | "[bypass_list]" =>
hostnames = bypassHostnames
subnets = bypassSubnets
case "[white_list]" | "[proxy_list]" =>
case "[reject_all]" | "[bypass_all]" =>
case "[accept_all]" | "[proxy_all]" =>
hostnames = proxyHostnames
subnets = proxySubnets
case "[reject_all]" | "[bypass_all]" => bypass = true
case "[accept_all]" | "[proxy_all]" => bypass = false
case input if subnets != null && input.nonEmpty => try subnets += Subnet.fromString(input) catch {
case _: IllegalArgumentException => if (isUrl(input)) {
urls += input
} else {
hostnames += input
}
case _: IllegalArgumentException => hostnames += input
}
case _ =>
}
this.subnets ++= (if (bypass) proxySubnets else bypassSubnets)
this
}
final def fromId(id: String): Acl = fromSource(Source.fromFile(Acl.getFile(id)))

def getAclString(network: Boolean): String = {
val result = new StringBuilder()
if (urls.nonEmpty) {
result.append(urls.mkString("\n"))
if (network) {
result.append("\n#NETWORK_ACL_BEGIN\n")
try {
urls.foreach((url: String) => result.append(Source.fromURL(url).mkString))
} catch {
case e: IOException => // ignore
}
result.append("\n#NETWORK_ACL_END\n")
def flatten(depth: Int): Acl = {
if (depth > 0) for (url <- urls) {
val child = new Acl().fromSource(Source.fromURL(url), bypass).flatten(depth - 1)
if (bypass != child.bypass) {
Log.w(TAG, "Imported network ACL has a conflicting mode set. This will probably not work as intended. URL: %s"
.format(url))
child.subnets.clear() // subnets for the different mode are discarded
child.bypass = bypass
}
result.append("\n")
bypassHostnames ++= child.bypassHostnames
proxyHostnames ++= child.proxyHostnames
subnets ++= child.subnets
}
if (result.isEmpty) {
result.append("[bypass_all]\n")
urls.clear()
this
}

override def toString: String = {
val result = new StringBuilder()
result.append(if (bypass) "[bypass_all]\n" else "[proxy_all]\n")
val (bypassList, proxyList) =
if (bypass) (bypassHostnames.toStream, subnets.toStream.map(_.toString) #::: proxyHostnames.toStream)
else (subnets.toStream.map(_.toString) #::: bypassHostnames.toStream, proxyHostnames.toStream)
if (bypassList.nonEmpty) {
result.append("[bypass_list]\n")
result.append(bypassList.mkString("\n"))
result.append('\n')
}
val list = subnets.toStream.map(_.toString) #::: hostnames.toStream
if (list.nonEmpty) {
if (proxyList.nonEmpty) {
result.append("[proxy_list]\n")
result.append(list.mkString("\n"))
result.append(proxyList.mkString("\n"))
result.append('\n')
}
result.append(urls.map("#IMPORT_URL <%s>\n".format(_)).mkString)
result.toString
}

override def toString: String = {
getAclString(false)
}

def isValidCustomRules: Boolean = !hostnames.isEmpty

// Don't change: dummy fields for OrmLite interaction

// noinspection ScalaUnusedSymbol
Expand All @@ -125,23 +155,28 @@ class Acl {
}

object Acl {
final val TAG = "Acl"
final val ALL = "all"
final val BYPASS_LAN = "bypass-lan"
final val BYPASS_CHN = "bypass-china"
final val BYPASS_LAN_CHN = "bypass-lan-china"
final val GFWLIST = "gfwlist"
final val CHINALIST = "china-list"
final val CUSTOM_RULES = "custom-rules"
final val CUSTOM_RULES_FLATTENED = "custom-rules-flattened"
private val networkAclParser = "^IMPORT_URL\\s*<(.+)>\\s*$".r

private val urlOrdering: Ordering[URL] = Ordering.by(url => (url.getHost, url.getPort, url.getFile, url.getProtocol))

def getFile(id: String) = new File(app.getFilesDir, id + ".acl")
def customRules: Acl = {
val acl = new Acl()
try acl.fromId(CUSTOM_RULES) catch {
case _: FileNotFoundException =>
}
acl.bypass = true
acl.bypassHostnames.clear() // everything is bypassed
acl
}
def save(id: String, acl: Acl, network: Boolean = false): Unit = {
IOUtils.writeString(getFile(id), acl.getAclString(network))
}
def save(id: String, acl: Acl): Unit = IOUtils.writeString(getFile(id), acl.toString)
}
Loading

0 comments on commit b8d7ade

Please sign in to comment.