diff --git a/app/src/main/java/io/nekohasekai/sagernet/SagerNet.kt b/app/src/main/java/io/nekohasekai/sagernet/SagerNet.kt index b65a353..2cc881b 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/SagerNet.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/SagerNet.kt @@ -29,6 +29,7 @@ import libcore.Libcore import moe.matsuri.nb4a.NativeInterface import moe.matsuri.nb4a.utils.JavaUtil import moe.matsuri.nb4a.utils.cleanWebview +import java.io.File import androidx.work.Configuration as WorkConfiguration class SagerNet : Application(), @@ -40,11 +41,11 @@ class SagerNet : Application(), application = this } - val nativeInterface = NativeInterface() + private val nativeInterface = NativeInterface() - val externalAssets by lazy { getExternalFilesDir(null) ?: filesDir } - val process = JavaUtil.getProcessName() - val isMainProcess = process == BuildConfig.APPLICATION_ID + val externalAssets: File by lazy { getExternalFilesDir(null) ?: filesDir } + val process: String = JavaUtil.getProcessName() + private val isMainProcess = process == BuildConfig.APPLICATION_ID val isBgProcess = process.endsWith(":bg") override fun onCreate() { diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt b/app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt index 30c9a68..1715765 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt @@ -26,11 +26,8 @@ import io.nekohasekai.sagernet.fmt.wireguard.buildSingBoxOutboundWireguardBean import io.nekohasekai.sagernet.ktx.isIpAddress import io.nekohasekai.sagernet.ktx.mkPort import io.nekohasekai.sagernet.utils.PackageCache -import moe.matsuri.nb4a.Protocols +import moe.matsuri.nb4a.* import moe.matsuri.nb4a.SingBoxOptions.* -import moe.matsuri.nb4a.SingBoxOptionsUtil -import moe.matsuri.nb4a.checkEmpty -import moe.matsuri.nb4a.makeSingBoxRule import moe.matsuri.nb4a.plugin.Plugins import moe.matsuri.nb4a.proxy.config.ConfigBean import moe.matsuri.nb4a.proxy.shadowtls.ShadowTLSBean @@ -535,6 +532,9 @@ fun buildConfig( if (rule.ip.isNotBlank()) { makeSingBoxRule(rule.ip.listByLineOrComma(), true) } + + generateRuleSet() + if (rule.port.isNotBlank()) { port = mutableListOf() port_range = mutableListOf() @@ -733,7 +733,7 @@ fun buildConfig( if (DataStore.bypassLanInCore) { route.rules.add(Rule_DefaultOptions().apply { outbound = TAG_BYPASS - geoip = listOf("private") + ip_is_private = true }) } // block mcast diff --git a/app/src/main/java/io/nekohasekai/sagernet/utils/GeoipUtils.kt b/app/src/main/java/io/nekohasekai/sagernet/utils/GeoipUtils.kt new file mode 100644 index 0000000..519c9c9 --- /dev/null +++ b/app/src/main/java/io/nekohasekai/sagernet/utils/GeoipUtils.kt @@ -0,0 +1,25 @@ +package io.nekohasekai.sagernet.utils + +import android.content.Context +import io.nekohasekai.sagernet.ktx.app +import libcore.Libcore +import java.io.File + +object GeoipUtils { + fun generateRuleSet(context: Context = app.applicationContext, country: String) { + + val filesDir = context.getExternalFilesDir(null) ?: context.filesDir + + val ruleSetDir = filesDir.resolve("ruleSets") + ruleSetDir.mkdirs() + + val geositeFile = File(filesDir, "geoip.db") + + val geoip = Libcore.newGeoip() + if (!geoip.openGeosite(geositeFile.absolutePath)) { + error("open geoip failed") + } + + geoip.convertGeoip(country, ruleSetDir.resolve("geoip-$country.srs").absolutePath) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/nekohasekai/sagernet/utils/GeositeUtils.kt b/app/src/main/java/io/nekohasekai/sagernet/utils/GeositeUtils.kt new file mode 100644 index 0000000..740170c --- /dev/null +++ b/app/src/main/java/io/nekohasekai/sagernet/utils/GeositeUtils.kt @@ -0,0 +1,25 @@ +package io.nekohasekai.sagernet.utils + +import android.content.Context +import io.nekohasekai.sagernet.ktx.app +import libcore.Geosite +import java.io.File + +object GeositeUtils { + fun generateRuleSet(context: Context = app.applicationContext, code: String) { + + val filesDir = context.getExternalFilesDir(null) ?: context.filesDir + + val ruleSetDir = filesDir.resolve("ruleSets") + ruleSetDir.mkdirs() + + val geositeFile = File(filesDir, "geosite.db") + + val geosite = Geosite() + if (!geosite.checkGeositeCode(geositeFile.absolutePath, code)) { + error("code $code not found in geosite") + } + + geosite.convertGeosite(code, ruleSetDir.resolve("geosite-$code.srs").absolutePath) + } +} \ No newline at end of file diff --git a/app/src/main/java/moe/matsuri/nb4a/SingBoxOptions.java b/app/src/main/java/moe/matsuri/nb4a/SingBoxOptions.java index a0b0e05..6148ced 100644 --- a/app/src/main/java/moe/matsuri/nb4a/SingBoxOptions.java +++ b/app/src/main/java/moe/matsuri/nb4a/SingBoxOptions.java @@ -968,12 +968,10 @@ public class SingBoxOptions { public static class RouteOptions extends SingBoxOption { - public GeoIPOptions geoip; - - public GeositeOptions geosite; - public List rules; + public List rule_set; + @SerializedName("final") public String final_; @@ -989,26 +987,6 @@ public class SingBoxOptions { } - public static class GeoIPOptions extends SingBoxOption { - - public String path; - - public String download_url; - - public String download_detour; - - } - - public static class GeositeOptions extends SingBoxOption { - - public String path; - - public String download_url; - - public String download_detour; - - } - public static class Rule extends SingBoxOption { @@ -1020,6 +998,20 @@ public class SingBoxOptions { } + public static class RuleSet extends SingBoxOption { + + public String type; + + public String tag; + + public String format; + + public String path; + + public String url; + + } + public static class DefaultRule extends SingBoxOption { // Generate note: Listable @@ -1048,15 +1040,6 @@ public class SingBoxOptions { // Generate note: Listable public List domain_regex; - // Generate note: Listable - public List geosite; - - // Generate note: Listable - public List source_geoip; - - // Generate note: Listable - public List geoip; - // Generate note: Listable public List source_ip_cidr; @@ -1098,18 +1081,6 @@ public class SingBoxOptions { } - public static class LogicalRule extends SingBoxOption { - - public String mode; - - public List rules; - - public Boolean invert; - - public String outbound; - - } - public static class DNSRule extends SingBoxOption { @@ -1155,9 +1126,6 @@ public class SingBoxOptions { // Generate note: Listable public List geosite; - // Generate note: Listable - public List source_geoip; - // Generate note: Listable public List source_ip_cidr; @@ -1203,22 +1171,6 @@ public class SingBoxOptions { } - public static class LogicalDNSRule extends SingBoxOption { - - public String mode; - - public List rules; - - public Boolean invert; - - public String server; - - public Boolean disable_cache; - - public Integer rewrite_ttl; - - } - public static class ShadowsocksInboundOptions extends SingBoxOption { // Generate note: nested type ListenOptions @@ -4387,14 +4339,12 @@ public class SingBoxOptions { // Generate note: Listable public List domain_regex; - // Generate note: Listable - public List geosite; + public List rule_set; - // Generate note: Listable - public List source_geoip; + public Boolean source_ip_is_private; - // Generate note: Listable - public List geoip; + public Boolean rule_set_ipcidr_match_source; + public Boolean ip_is_private; // Generate note: Listable public List source_ip_cidr; @@ -4437,18 +4387,6 @@ public class SingBoxOptions { } - public static class Rule_LogicalOptions extends Rule { - - public String mode; - - public List rules; - - public Boolean invert; - - public String outbound; - - } - public static class DNSRule_DefaultOptions extends DNSRule { // Generate note: Listable @@ -4483,9 +4421,6 @@ public class SingBoxOptions { // Generate note: Listable public List geosite; - // Generate note: Listable - public List source_geoip; - // Generate note: Listable public List source_ip_cidr; @@ -4531,22 +4466,6 @@ public class SingBoxOptions { } - public static class DNSRule_LogicalOptions extends DNSRule { - - public String mode; - - public List rules; - - public Boolean invert; - - public String server; - - public Boolean disable_cache; - - public Integer rewrite_ttl; - - } - public static class V2RayTransportOptions_HTTPOptions extends V2RayTransportOptions { // Generate note: Listable diff --git a/app/src/main/java/moe/matsuri/nb4a/SingBoxOptionsUtil.kt b/app/src/main/java/moe/matsuri/nb4a/SingBoxOptionsUtil.kt index 872042f..80375c5 100644 --- a/app/src/main/java/moe/matsuri/nb4a/SingBoxOptionsUtil.kt +++ b/app/src/main/java/moe/matsuri/nb4a/SingBoxOptionsUtil.kt @@ -1,6 +1,8 @@ package moe.matsuri.nb4a import io.nekohasekai.sagernet.database.DataStore +import io.nekohasekai.sagernet.utils.GeoipUtils +import io.nekohasekai.sagernet.utils.GeositeUtils object SingBoxOptionsUtil { @@ -70,12 +72,26 @@ fun SingBoxOptions.DNSRule_DefaultOptions.checkEmpty(): Boolean { return true } +fun SingBoxOptions.Rule_DefaultOptions.generateRuleSet() { + rule_set.forEach { + when { + it.startsWith("geoip") -> { + GeoipUtils.generateRuleSet(country = it.removePrefix("geoip:")) + } + + it.startsWith("geosite") -> { + GeositeUtils.generateRuleSet(code = it.removePrefix("geosite:")) + } + } + } +} + fun SingBoxOptions.Rule_DefaultOptions.makeSingBoxRule(list: List, isIP: Boolean) { if (isIP) { ip_cidr = mutableListOf() - geoip = mutableListOf() + rule_set = mutableListOf() } else { - geosite = mutableListOf() + rule_set = mutableListOf() domain = mutableListOf() domain_suffix = mutableListOf() domain_regex = mutableListOf() @@ -84,14 +100,15 @@ fun SingBoxOptions.Rule_DefaultOptions.makeSingBoxRule(list: List, isIP: list.forEach { if (isIP) { if (it.startsWith("geoip:")) { - geoip.plusAssign(it.removePrefix("geoip:")) + rule_set.plusAssign(it) + rule_set_ipcidr_match_source = true } else { ip_cidr.plusAssign(it) } return@forEach } if (it.startsWith("geosite:")) { - geosite.plusAssign(it.removePrefix("geosite:")) + rule_set.plusAssign(it) } else if (it.startsWith("full:")) { domain.plusAssign(it.removePrefix("full:").lowercase()) } else if (it.startsWith("domain:")) { @@ -106,15 +123,12 @@ fun SingBoxOptions.Rule_DefaultOptions.makeSingBoxRule(list: List, isIP: } } ip_cidr?.removeIf { it.isNullOrBlank() } - geoip?.removeIf { it.isNullOrBlank() } - geosite?.removeIf { it.isNullOrBlank() } + rule_set?.removeIf { it.isNullOrBlank() } domain?.removeIf { it.isNullOrBlank() } domain_suffix?.removeIf { it.isNullOrBlank() } domain_regex?.removeIf { it.isNullOrBlank() } domain_keyword?.removeIf { it.isNullOrBlank() } if (ip_cidr?.isEmpty() == true) ip_cidr = null - if (geoip?.isEmpty() == true) geoip = null - if (geosite?.isEmpty() == true) geosite = null if (domain?.isEmpty() == true) domain = null if (domain_suffix?.isEmpty() == true) domain_suffix = null if (domain_regex?.isEmpty() == true) domain_regex = null @@ -123,9 +137,8 @@ fun SingBoxOptions.Rule_DefaultOptions.makeSingBoxRule(list: List, isIP: fun SingBoxOptions.Rule_DefaultOptions.checkEmpty(): Boolean { if (ip_cidr?.isNotEmpty() == true) return false - if (geoip?.isNotEmpty() == true) return false - if (geosite?.isNotEmpty() == true) return false if (domain?.isNotEmpty() == true) return false + if (rule_set?.isNotEmpty() == true) return false if (domain_suffix?.isNotEmpty() == true) return false if (domain_regex?.isNotEmpty() == true) return false if (domain_keyword?.isNotEmpty() == true) return false diff --git a/libcore/geoip.go b/libcore/geoip.go new file mode 100644 index 0000000..ccc6577 --- /dev/null +++ b/libcore/geoip.go @@ -0,0 +1,78 @@ +package libcore + +import ( + "github.com/oschwald/maxminddb-golang" + "github.com/sagernet/sing-box/common/srs" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "log" + "net" + "os" + "strings" +) + +type Geoip struct { + geoipReader *maxminddb.Reader +} + +func (g *Geoip) OpenGeosite(path string) bool { + geoipReader, err := maxminddb.Open(path) + g.geoipReader = geoipReader + if err != nil { + log.Println("failed to open geoip file:", err) + return false + } else { + log.Println("loaded geoip database") + } + return true +} + +func (g *Geoip) ConvertGeoip(countryCode, outputPath string) { + networks := g.geoipReader.Networks(maxminddb.SkipAliasedNetworks) + countryMap := make(map[string][]*net.IPNet) + var ( + ipNet *net.IPNet + nextCountryCode string + err error + ) + for networks.Next() { + ipNet, err = networks.Network(&nextCountryCode) + if err != nil { + log.Println("failed to get network:", err) + return + } + countryMap[nextCountryCode] = append(countryMap[nextCountryCode], ipNet) + } + + ipNets := countryMap[strings.ToLower(countryCode)] + + if len(ipNets) == 0 { + log.Println("no networks found for country code:", countryCode) + return + } + + var headlessRule option.DefaultHeadlessRule + headlessRule.IPCIDR = make([]string, 0, len(ipNets)) + for _, cidr := range ipNets { + headlessRule.IPCIDR = append(headlessRule.IPCIDR, cidr.String()) + } + var plainRuleSet option.PlainRuleSetCompat + plainRuleSet.Version = C.RuleSetVersion1 + plainRuleSet.Options.Rules = []option.HeadlessRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: headlessRule, + }, + } + + outputFile, err := os.Create(outputPath) + err = srs.Write(outputFile, plainRuleSet.Upgrade()) + if err != nil { + log.Println("failed to write geosite file:", err) + return + } +} + +func NewGeoip() *Geoip { + return new(Geoip) +} diff --git a/libcore/geosite.go b/libcore/geosite.go new file mode 100644 index 0000000..6b128d7 --- /dev/null +++ b/libcore/geosite.go @@ -0,0 +1,70 @@ +package libcore + +import ( + "github.com/sagernet/sing-box/common/geosite" + "github.com/sagernet/sing-box/common/srs" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "os" + + "log" +) + +type Geosite struct { + geositeReader *geosite.Reader +} + +func (g *Geosite) CheckGeositeCode(path string, code string) bool { + geositeReader, codes, err := geosite.Open(path) + g.geositeReader = geositeReader + if err != nil { + log.Println("failed to open geosite file:", err) + return false + } else { + log.Println("loaded geosite database: ", len(codes), " codes") + } + sourceSet, err := geositeReader.Read(code) + if err != nil { + log.Println("failed to read geosite code:", code, err) + return false + } + return len(sourceSet) >= 1 +} + +// ConvertGeosite need to run CheckGeositeCode first +func (g *Geosite) ConvertGeosite(code string, outputPath string) { + + sourceSet, err := g.geositeReader.Read(code) + if err != nil { + log.Println("failed to read geosite code:", code, err) + return + } + + var headlessRule option.DefaultHeadlessRule + + defaultRule := geosite.Compile(sourceSet) + + headlessRule.Domain = defaultRule.Domain + headlessRule.DomainSuffix = defaultRule.DomainSuffix + headlessRule.DomainKeyword = defaultRule.DomainKeyword + headlessRule.DomainRegex = defaultRule.DomainRegex + var plainRuleSet option.PlainRuleSetCompat + plainRuleSet.Version = C.RuleSetVersion1 + plainRuleSet.Options.Rules = []option.HeadlessRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: headlessRule, + }, + } + + outputFile, err := os.Create(outputPath) + err = srs.Write(outputFile, plainRuleSet.Upgrade()) + if err != nil { + log.Println("failed to write geosite file:", err) + return + } +} + +func newGeosite() *Geosite { + return new(Geosite) +} diff --git a/libcore/go.mod b/libcore/go.mod index 6c7550b..ef6e8eb 100644 --- a/libcore/go.mod +++ b/libcore/go.mod @@ -14,6 +14,8 @@ require ( golang.org/x/mobile v0.0.0-20231108233038-35478a0c49da ) +require github.com/oschwald/maxminddb-golang v1.12.0 + require ( berty.tech/go-libtor v1.0.385 // indirect github.com/ajg/form v1.5.1 // indirect @@ -48,7 +50,6 @@ require ( github.com/mholt/acmez v1.2.0 // indirect github.com/onsi/ginkgo/v2 v2.9.7 // indirect github.com/ooni/go-libtor v1.1.8 // indirect - github.com/oschwald/maxminddb-golang v1.12.0 // indirect github.com/pierrec/lz4/v4 v4.1.14 // indirect github.com/quic-go/qpack v0.4.0 // indirect github.com/quic-go/qtls-go1-20 v0.4.1 // indirect diff --git a/libcore/go.sum b/libcore/go.sum index 476da08..5283246 100644 --- a/libcore/go.sum +++ b/libcore/go.sum @@ -6,8 +6,6 @@ github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sx github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/caddyserver/certmagic v0.20.0 h1:bTw7LcEZAh9ucYCRXyCpIrSAGplplI0vGYJ4BpCQ/Fc= github.com/caddyserver/certmagic v0.20.0/go.mod h1:N4sXgpICQUskEWpj7zVzvWD41p3NYacrNoZYiRM2jTg= -github.com/cloudflare/circl v1.3.6 h1:/xbKIqSHbZXHwkhbrhrt2YOHIwYJlXH94E3tI/gDlUg= -github.com/cloudflare/circl v1.3.6/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=