Browse Source

Local dns & route

世界 4 years ago
parent
commit
bbf24bf785
29 changed files with 736 additions and 256 deletions
  1. 2 1
      .gitignore
  2. 1 0
      .idea/vcs.xml
  3. 2 2
      app/src/main/AndroidManifest.xml
  4. 1 3
      app/src/main/java/com/github/shadowsocks/preference/PluginConfigurationDialogFragment.kt
  5. 29 9
      app/src/main/java/io/nekohasekai/sagernet/Constants.kt
  6. 13 0
      app/src/main/java/io/nekohasekai/sagernet/SagerNet.kt
  7. 3 13
      app/src/main/java/io/nekohasekai/sagernet/bg/ProxyInstance.kt
  8. 32 171
      app/src/main/java/io/nekohasekai/sagernet/bg/VpnService.kt
  9. 38 16
      app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt
  10. 3 3
      app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt
  11. 3 0
      app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/V2rayConfig.java
  12. 158 26
      app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/VMessFmt.kt
  13. 10 2
      app/src/main/java/io/nekohasekai/sagernet/ktx/Preferences.kt
  14. 15 3
      app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt
  15. 5 0
      app/src/main/java/io/nekohasekai/sagernet/ui/MainActivity.kt
  16. 22 0
      app/src/main/java/io/nekohasekai/sagernet/ui/SettingsFragment.kt
  17. 82 0
      app/src/main/java/io/nekohasekai/sagernet/ui/SettingsPreferenceFragment.kt
  18. 1 1
      app/src/main/java/io/nekohasekai/sagernet/ui/profile/ProfileSettingsActivity.kt
  19. 1 1
      app/src/main/java/io/nekohasekai/sagernet/ui/profile/ShadowsocksSettingsActivity.kt
  20. 1 1
      app/src/main/java/io/nekohasekai/sagernet/ui/profile/SocksSettingsActivity.kt
  21. 1 1
      app/src/main/java/io/nekohasekai/sagernet/widget/QRCodeDialog.kt
  22. 13 0
      app/src/main/res/drawable/ic_baseline_nat_24.xml
  23. 6 1
      app/src/main/res/menu/activity_main_drawer.xml
  24. 6 0
      app/src/main/res/navigation/mobile_navigation.xml
  25. 187 0
      app/src/main/res/values/arrays.xml
  26. 10 1
      app/src/main/res/values/strings.xml
  27. 86 0
      app/src/main/res/xml/global_preferences.xml
  28. 4 0
      bin/update_geofile.sh
  29. 1 1
      v2ray

+ 2 - 1
.gitignore

@@ -13,4 +13,5 @@ build/
 .externalNativeBuild
 .cxx
 local.properties
-/app/libs/
+/app/libs/
+/app/src/main/assets/*.dat

+ 1 - 0
.idea/vcs.xml

@@ -4,6 +4,7 @@
     <mapping directory="$PROJECT_DIR$" vcs="Git" />
     <mapping directory="$PROJECT_DIR$/app/src/main/jni/badvpn" vcs="Git" />
     <mapping directory="$PROJECT_DIR$/app/src/main/jni/libancillary" vcs="Git" />
+    <mapping directory="$PROJECT_DIR$/shadowsocks/src/main/rust/shadowsocks-rust" vcs="Git" />
     <mapping directory="$PROJECT_DIR$/v2ray" vcs="Git" />
   </component>
 </project>

+ 2 - 2
app/src/main/AndroidManifest.xml

@@ -41,8 +41,8 @@
             </intent-filter>
         </activity>
         <activity android:name=".ui.VpnRequestActivity" />
-        <activity android:name=".ui.settings.SocksSettingsActivity" />
-        <activity android:name=".ui.settings.ShadowsocksSettingsActivity" />
+        <activity android:name=".ui.profile.SocksSettingsActivity" />
+        <activity android:name=".ui.profile.ShadowsocksSettingsActivity" />
 
         <service
             android:name=".bg.ProxyService"

+ 1 - 3
app/src/main/java/com/github/shadowsocks/preference/PluginConfigurationDialogFragment.kt

@@ -28,9 +28,7 @@ import androidx.preference.EditTextPreferenceDialogFragmentCompat
 import androidx.preference.PreferenceDialogFragmentCompat
 import com.github.shadowsocks.plugin.PluginContract
 import com.github.shadowsocks.plugin.PluginManager
-import io.nekohasekai.sagernet.fmt.shadowsocks.ShadowsocksBean
-import io.nekohasekai.sagernet.ui.settings.ProfileSettingsActivity
-import io.nekohasekai.sagernet.ui.settings.ShadowsocksSettingsActivity
+import io.nekohasekai.sagernet.ui.profile.ShadowsocksSettingsActivity
 
 class PluginConfigurationDialogFragment : EditTextPreferenceDialogFragmentCompat() {
     companion object {

+ 29 - 9
app/src/main/java/io/nekohasekai/sagernet/Constants.kt

@@ -6,10 +6,23 @@ object Key {
     const val DB_PROFILE = "sager_net.db"
     const val DISABLE_AEAD = "V2RAY_VMESS_AEAD_DISABLED"
 
-    const val SERVICE_MODE = "service_mode"
-    const val MODE_VPN = 0
-    const val MODE_PROXY = 1
-    const val MODE_TRANS = 2
+    const val SERVICE_MODE = "serviceMode"
+    const val MODE_VPN = "vpn"
+    const val MODE_PROXY = "proxy"
+    const val MODE_TRANS = "transproxy"
+
+    const val REMOTE_DNS = "remoteDns"
+    const val ENABLE_LOCAL_DNS = "enableLocalDns"
+    const val LOCAL_DNS_PORT = "portLocalDns"
+    const val DOMESTIC_DNS = "domesticDns"
+
+    const val IPV6_ROUTE = "ipv6Route"
+    const val PREFER_IPV6 = "preferIpv6"
+
+    const val ROUTE_MODE = "routeMode"
+    const val SOCKS_PORT = "socksPort"
+
+    const val ALLOW_ACCESS = "allowAccess"
 
     const val PROFILE_DIRTY = "profileDirty"
     const val PROFILE_ID = "profileId"
@@ -27,11 +40,18 @@ object Key {
 
 }
 
+object RouteMode {
+    const val ALL = "all"
+    const val BYPASS_LAN = "bypass-lan"
+    const val BYPASS_CHINA = "bypass-china"
+    const val BYPASS_LAN_CHINA = "bypass-lan-china"
+}
+
 object Action {
-    const val SERVICE = "com.github.shadowsocks.SERVICE"
-    const val CLOSE = "com.github.shadowsocks.CLOSE"
-    const val RELOAD = "com.github.shadowsocks.RELOAD"
-    const val ABORT = "com.github.shadowsocks.ABORT"
+    const val SERVICE = "io.nekohasekai.sagernet.SERVICE"
+    const val CLOSE = "io.nekohasekai.sagernet.CLOSE"
+    const val RELOAD = "io.nekohasekai.sagernet.RELOAD"
+    const val ABORT = "io.nekohasekai.sagernet.ABORT"
 
-    const val EXTRA_PROFILE_ID = "com.github.shadowsocks.EXTRA_PROFILE_ID"
+    const val EXTRA_PROFILE_ID = "io.nekohasekai.sagernet.EXTRA_PROFILE_ID"
 }

+ 13 - 0
app/src/main/java/io/nekohasekai/sagernet/SagerNet.kt

@@ -15,11 +15,16 @@ import android.os.UserManager
 import androidx.annotation.RequiresApi
 import androidx.core.content.ContextCompat
 import androidx.core.content.getSystemService
+import go.Seq
 import io.nekohasekai.sagernet.bg.SagerConnection
 import io.nekohasekai.sagernet.database.DataStore
 import io.nekohasekai.sagernet.ui.MainActivity
 import io.nekohasekai.sagernet.utils.DeviceStorageApp
+import kotlinx.coroutines.DEBUG_PROPERTY_NAME
+import kotlinx.coroutines.DEBUG_PROPERTY_VALUE_ON
+import libv2ray.Libv2ray
 import me.weishu.reflection.Reflection
+import java.io.File
 
 class SagerNet : Application() {
 
@@ -93,8 +98,16 @@ class SagerNet : Application() {
 
     override fun onCreate() {
         super.onCreate()
+        System.setProperty(DEBUG_PROPERTY_NAME, DEBUG_PROPERTY_VALUE_ON)
+
         DataStore.init()
         updateNotificationChannels()
+
+        Seq.setContext(applicationContext)
+        Libv2ray.setAssetsPath(
+            File(application.filesDir, "geofile").absolutePath,
+            "geofile/"
+        )
     }
 
     fun getPackageInfo(packageName: String) = packageManager.getPackageInfo(packageName,

+ 3 - 13
app/src/main/java/io/nekohasekai/sagernet/bg/ProxyInstance.kt

@@ -27,19 +27,17 @@ class ProxyInstance(val profile: ProxyEntity) {
     lateinit var config: V2rayConfig
     lateinit var base: BaseService.Interface
 
-    val bind get() = if (DataStore.allowAccess) "0.0.0.0" else "127.0.0.1"
-
     fun init(service: BaseService.Interface) {
         base = service
         v2rayPoint = Libv2ray.newV2RayPoint(SagerSupportClass(if (service is VpnService)
             service else null), false)
         if (profile.useExternalShadowsocks()) {
-            v2rayPoint.domainName = "127.0.0.1:${DataStore.socks5Port + 10}"
+            v2rayPoint.domainName = "127.0.0.1:${DataStore.socksPort + 10}"
         } else {
             v2rayPoint.domainName =
                 profile.requireBean().serverAddress + ":" + profile.requireBean().serverPort
         }
-        config = buildV2rayConfig(profile, bind, DataStore.socks5Port)
+        config = buildV2rayConfig(profile)
         v2rayPoint.configureFileContent = gson.toJson(config).also {
             Logs.d(it)
         }
@@ -50,7 +48,7 @@ class ProxyInstance(val profile: ProxyEntity) {
     fun start() {
         if (profile.useExternalShadowsocks()) {
             val bean = profile.requireSS()
-            val port = DataStore.socks5Port + 10
+            val port = DataStore.socksPort + 10
 
             val proxyConfig = JSONObject().also {
                 it["server"] = bean.serverAddress
@@ -151,18 +149,10 @@ class ProxyInstance(val profile: ProxyEntity) {
             return 0L
         }
 
-        override fun prepare(): Long {
-            return 0L
-        }
-
         override fun protect(l: Long): Boolean {
             return (service ?: return true).protect(l.toInt())
         }
 
-        override fun setup(p0: String?): Long {
-            return 0
-        }
-
         override fun shutdown(): Long {
             return 0
         }

+ 32 - 171
app/src/main/java/io/nekohasekai/sagernet/bg/VpnService.kt

@@ -10,6 +10,7 @@ import android.system.ErrnoException
 import android.system.Os
 import io.nekohasekai.sagernet.Key
 import io.nekohasekai.sagernet.R
+import io.nekohasekai.sagernet.RouteMode
 import io.nekohasekai.sagernet.SagerNet
 import io.nekohasekai.sagernet.database.DataStore
 import io.nekohasekai.sagernet.ui.VpnRequestActivity
@@ -30,164 +31,6 @@ class VpnService : BaseVpnService(), BaseService.Interface {
         private const val PRIVATE_VLAN6_CLIENT = "fdfe:dcba:9876::1"
         private const val PRIVATE_VLAN6_ROUTER = "fdfe:dcba:9876::2"
 
-        private val PRIVATE_ROUTES = arrayOf(
-            "1.0.0.0/8",
-            "2.0.0.0/7",
-            "4.0.0.0/6",
-            "8.0.0.0/7",
-            "11.0.0.0/8",
-            "12.0.0.0/6",
-            "16.0.0.0/4",
-            "32.0.0.0/3",
-            "64.0.0.0/3",
-            "96.0.0.0/6",
-            "100.0.0.0/10",
-            "100.128.0.0/9",
-            "101.0.0.0/8",
-            "102.0.0.0/7",
-            "104.0.0.0/5",
-            "112.0.0.0/10",
-            "112.64.0.0/11",
-            "112.96.0.0/12",
-            "112.112.0.0/13",
-            "112.120.0.0/14",
-            "112.124.0.0/19",
-            "112.124.32.0/21",
-            "112.124.40.0/22",
-            "112.124.44.0/23",
-            "112.124.46.0/24",
-            "112.124.48.0/20",
-            "112.124.64.0/18",
-            "112.124.128.0/17",
-            "112.125.0.0/16",
-            "112.126.0.0/15",
-            "112.128.0.0/9",
-            "113.0.0.0/8",
-            "114.0.0.0/10",
-            "114.64.0.0/11",
-            "114.96.0.0/12",
-            "114.112.0.0/15",
-            "114.114.0.0/18",
-            "114.114.64.0/19",
-            "114.114.96.0/20",
-            "114.114.112.0/23",
-            "114.114.115.0/24",
-            "114.114.116.0/22",
-            "114.114.120.0/21",
-            "114.114.128.0/17",
-            "114.115.0.0/16",
-            "114.116.0.0/14",
-            "114.120.0.0/13",
-            "114.128.0.0/9",
-            "115.0.0.0/8",
-            "116.0.0.0/6",
-            "120.0.0.0/6",
-            "124.0.0.0/7",
-            "126.0.0.0/8",
-            "128.0.0.0/3",
-            "160.0.0.0/5",
-            "168.0.0.0/8",
-            "169.0.0.0/9",
-            "169.128.0.0/10",
-            "169.192.0.0/11",
-            "169.224.0.0/12",
-            "169.240.0.0/13",
-            "169.248.0.0/14",
-            "169.252.0.0/15",
-            "169.255.0.0/16",
-            "170.0.0.0/7",
-            "172.0.0.0/12",
-            "172.32.0.0/11",
-            "172.64.0.0/10",
-            "172.128.0.0/9",
-            "173.0.0.0/8",
-            "174.0.0.0/7",
-            "176.0.0.0/4",
-            "192.0.0.8/29",
-            "192.0.0.16/28",
-            "192.0.0.32/27",
-            "192.0.0.64/26",
-            "192.0.0.128/25",
-            "192.0.1.0/24",
-            "192.0.3.0/24",
-            "192.0.4.0/22",
-            "192.0.8.0/21",
-            "192.0.16.0/20",
-            "192.0.32.0/19",
-            "192.0.64.0/18",
-            "192.0.128.0/17",
-            "192.1.0.0/16",
-            "192.2.0.0/15",
-            "192.4.0.0/14",
-            "192.8.0.0/13",
-            "192.16.0.0/12",
-            "192.32.0.0/11",
-            "192.64.0.0/12",
-            "192.80.0.0/13",
-            "192.88.0.0/18",
-            "192.88.64.0/19",
-            "192.88.96.0/23",
-            "192.88.98.0/24",
-            "192.88.100.0/22",
-            "192.88.104.0/21",
-            "192.88.112.0/20",
-            "192.88.128.0/17",
-            "192.89.0.0/16",
-            "192.90.0.0/15",
-            "192.92.0.0/14",
-            "192.96.0.0/11",
-            "192.128.0.0/11",
-            "192.160.0.0/13",
-            "192.169.0.0/16",
-            "192.170.0.0/15",
-            "192.172.0.0/14",
-            "192.176.0.0/12",
-            "192.192.0.0/10",
-            "193.0.0.0/8",
-            "194.0.0.0/7",
-            "196.0.0.0/7",
-            "198.0.0.0/12",
-            "198.16.0.0/15",
-            "198.20.0.0/14",
-            "198.24.0.0/13",
-            "198.32.0.0/12",
-            "198.48.0.0/15",
-            "198.50.0.0/16",
-            "198.51.0.0/18",
-            "198.51.64.0/19",
-            "198.51.96.0/22",
-            "198.51.101.0/24",
-            "198.51.102.0/23",
-            "198.51.104.0/21",
-            "198.51.112.0/20",
-            "198.51.128.0/17",
-            "198.52.0.0/14",
-            "198.56.0.0/13",
-            "198.64.0.0/10",
-            "198.128.0.0/9",
-            "199.0.0.0/8",
-            "200.0.0.0/7",
-            "202.0.0.0/8",
-            "203.0.0.0/18",
-            "203.0.64.0/19",
-            "203.0.96.0/20",
-            "203.0.112.0/24",
-            "203.0.114.0/23",
-            "203.0.116.0/22",
-            "203.0.120.0/21",
-            "203.0.128.0/17",
-            "203.1.0.0/16",
-            "203.2.0.0/15",
-            "203.4.0.0/14",
-            "203.8.0.0/13",
-            "203.16.0.0/12",
-            "203.32.0.0/11",
-            "203.64.0.0/10",
-            "203.128.0.0/9",
-            "204.0.0.0/6",
-            "208.0.0.0/4",
-        )
-
         private fun <T> FileDescriptor.use(block: (FileDescriptor) -> T) = try {
             block(this)
         } finally {
@@ -246,17 +89,29 @@ class VpnService : BaseVpnService(), BaseService.Interface {
             .setSession(profile.displayName())
             .setMtu(VPN_MTU)
             .addAddress(PRIVATE_VLAN4_CLIENT, 30)
+        if (DataStore.ipv6Route) {
+            builder.addAddress(PRIVATE_VLAN6_CLIENT, 126)
+        }
 
-        PRIVATE_ROUTES.forEach {
-            val subnet = Subnet.fromString(it)!!
-            builder.addRoute(subnet.address.hostAddress, subnet.prefixSize)
+        when (DataStore.routeMode) {
+            RouteMode.BYPASS_LAN, RouteMode.BYPASS_LAN_CHINA -> {
+                resources.getStringArray(R.array.bypass_private_route).forEach {
+                    val subnet = Subnet.fromString(it)!!
+                    builder.addRoute(subnet.address.hostAddress, subnet.prefixSize)
+                }
+                builder.addRoute(PRIVATE_VLAN4_ROUTER, 32)
+                // https://issuetracker.google.com/issues/149636790
+                if (DataStore.ipv6Route) builder.addRoute("2000::", 3)
+            }
+            else -> {
+                builder.addRoute("0.0.0.0", 0)
+                if (DataStore.ipv6Route) builder.addRoute("::", 0)
+            }
         }
 
-        builder.addRoute(PRIVATE_VLAN4_ROUTER, 32)
+
         // https://issuetracker.google.com/issues/149636790
-        if (DataStore.ipv6Route) {
-            builder.addRoute("2000::", 3)
-        }
+
 
         /* val proxyApps = when (profile.proxyApps) {
              0 -> DataStore.proxyApps > 0
@@ -286,7 +141,11 @@ class VpnService : BaseVpnService(), BaseService.Interface {
          }
     */
 
-        builder.addDnsServer("1.1.1.1")
+        if (DataStore.enableLocalDNS) {
+            builder.addDnsServer(PRIVATE_VLAN4_ROUTER)
+        } else {
+            builder.addDnsServer(DataStore.remoteDNS)
+        }
 
         builder.addDisallowedApplication("com.github.shadowsocks")
         builder.addDisallowedApplication(packageName)
@@ -308,23 +167,25 @@ class VpnService : BaseVpnService(), BaseService.Interface {
                 "--netif-ipaddr",
                 PRIVATE_VLAN4_ROUTER,
                 "--socks-server-addr",
-                "127.0.0.1:${DataStore.socks5Port}",
+                "127.0.0.1:${DataStore.socksPort}",
                 "--tunmtu",
                 VPN_MTU.toString(),
                 "--sock-path",
                 File(SagerNet.deviceStorage.noBackupFilesDir, "sock_path").canonicalPath,
                 "--loglevel", "debug")
+        if (DataStore.enableLocalDNS) {
+            cmd += "--dnsgw"
+            cmd += "127.0.0.1:${DataStore.localDNSPort}"
+        }
         if (DataStore.ipv6Route) {
             cmd += "--netif-ip6addr"
             cmd += PRIVATE_VLAN6_ROUTER
         }
         var enableUDP = false
         when (profile.type) {
-            "soocks" -> enableUDP = profile.requireSOCKS().udp
-        }
-        if (enableUDP) {
-            cmd += "--enable-udprelay"
+            "socks" -> enableUDP = profile.requireSOCKS().udp
         }
+        cmd += "--enable-udprelay"
         data.processes!!.start(cmd, onRestartCallback = {
             try {
                 sendFd(conn.fileDescriptor)

+ 38 - 16
app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt

@@ -1,16 +1,13 @@
 package io.nekohasekai.sagernet.database
 
+import android.os.Binder
 import android.os.Build
 import io.nekohasekai.sagernet.Key
+import io.nekohasekai.sagernet.RouteMode
 import io.nekohasekai.sagernet.SagerNet
 import io.nekohasekai.sagernet.database.preference.PublicDatabase
 import io.nekohasekai.sagernet.database.preference.RoomPreferenceDataStore
-import io.nekohasekai.sagernet.ktx.boolean
-import io.nekohasekai.sagernet.ktx.int
-import io.nekohasekai.sagernet.ktx.long
-import io.nekohasekai.sagernet.ktx.string
-import kotlinx.coroutines.DEBUG_PROPERTY_NAME
-import kotlinx.coroutines.DEBUG_PROPERTY_VALUE_ON
+import io.nekohasekai.sagernet.ktx.*
 
 object DataStore {
 
@@ -21,23 +18,48 @@ object DataStore {
         if (Build.VERSION.SDK_INT >= 24) {
             SagerNet.deviceStorage.moveDatabaseFrom(SagerNet.application, Key.DB_PUBLIC)
         }
+    }
+
+    var selectedProxy by configurationStore.long("selected_proxy")
+    var serviceMode by configurationStore.string(Key.SERVICE_MODE) { Key.MODE_VPN }
+    var routeMode by configurationStore.string(Key.ROUTE_MODE) { RouteMode.ALL }
+    var allowAccess by configurationStore.boolean(Key.ALLOW_ACCESS)
 
-        System.setProperty(DEBUG_PROPERTY_NAME, DEBUG_PROPERTY_VALUE_ON)
+    var enableLocalDNS by configurationStore.boolean(Key.ENABLE_LOCAL_DNS)
+    var remoteDNS by configurationStore.string(Key.REMOTE_DNS) { "1.1.1.1" }
+    var domesticDns by configurationStore.string(Key.DOMESTIC_DNS) { "223.5.5.5" }
 
+    // hopefully hashCode = mHandle doesn't change, currently this is true from KitKat to Nougat
+    private val userIndex by lazy { Binder.getCallingUserHandle().hashCode() }
+    var socksPort: Int
+        get() = getLocalPort(Key.SOCKS_PORT, 2080)
+        set(value) = saveLocalPort(Key.SOCKS_PORT, value)
+    var localDNSPort: Int
+        get() = getLocalPort(Key.LOCAL_DNS_PORT, 6450)
+        set(value) {
+            saveLocalPort(Key.LOCAL_DNS_PORT, value)
+        }
+
+    fun initGlobal() {
+        if (configurationStore.getString(Key.SOCKS_PORT) == null) socksPort = socksPort
+        if (configurationStore.getString(Key.LOCAL_DNS_PORT) == null) localDNSPort = localDNSPort
     }
 
-    var serviceMode by configurationStore.int(Key.SERVICE_MODE)
-    var selectedProxy by configurationStore.long("selected_proxy")
-    var allowAccess by configurationStore.boolean("allow_access")
-    var socks5Port by configurationStore.int("socks5_port") { 3389 }
-    var useHttp by configurationStore.boolean("use_http")
-    var httpPort by configurationStore.long("http_port")
-    var ipv6Route by configurationStore.boolean("ipv6_route")
-    var preferIpv6 by configurationStore.boolean("prefer_ipv6")
+
+    private fun getLocalPort(key: String, default: Int): Int {
+        return parsePort(configurationStore.getString(key), default + userIndex)
+    }
+
+    private fun saveLocalPort(key: String, value: Int) {
+        configurationStore.putString(key, "$value")
+    }
+
+    var ipv6Route by configurationStore.boolean(Key.IPV6_ROUTE)
+    var preferIpv6 by configurationStore.boolean(Key.PREFER_IPV6)
     var meteredNetwork by configurationStore.boolean("metered_network")
     var proxyApps by configurationStore.int("proxyApps")
     var individual by configurationStore.string("individual")
-    var forceShadowsocksRust = true// by configurationStore.boolean("forceShadowsocksRust")
+    var forceShadowsocksRust by configurationStore.boolean("forceShadowsocksRust")
 
     // cache
     var dirty by profileCacheStore.boolean(Key.PROFILE_DIRTY)

+ 3 - 3
app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt

@@ -9,9 +9,9 @@ import io.nekohasekai.sagernet.fmt.shadowsocks.methodsV2fly
 import io.nekohasekai.sagernet.fmt.socks.SOCKSBean
 import io.nekohasekai.sagernet.fmt.v2ray.VMessBean
 import io.nekohasekai.sagernet.ktx.Logs
-import io.nekohasekai.sagernet.ui.settings.ProfileSettingsActivity
-import io.nekohasekai.sagernet.ui.settings.ShadowsocksSettingsActivity
-import io.nekohasekai.sagernet.ui.settings.SocksSettingsActivity
+import io.nekohasekai.sagernet.ui.profile.ProfileSettingsActivity
+import io.nekohasekai.sagernet.ui.profile.ShadowsocksSettingsActivity
+import io.nekohasekai.sagernet.ui.profile.SocksSettingsActivity
 
 @Entity(tableName = "proxy_entities", indices = [
     Index("groupId", name = "groupId")

+ 3 - 0
app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/V2rayConfig.java

@@ -48,6 +48,9 @@ public class V2rayConfig {
             public String address;
             public Integer port;
             public String clientIp;
+            public Boolean skipFallback;
+            public List<String> domains;
+            public List<String> expectIPs;
 
         }
 

+ 158 - 26
app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/VMessFmt.kt

@@ -2,6 +2,8 @@ package io.nekohasekai.sagernet.fmt.v2ray
 
 import cn.hutool.core.codec.Base64
 import cn.hutool.json.JSONObject
+import io.nekohasekai.sagernet.RouteMode
+import io.nekohasekai.sagernet.database.DataStore
 import io.nekohasekai.sagernet.database.ProxyEntity
 import io.nekohasekai.sagernet.fmt.shadowsocks.ShadowsocksBean
 import io.nekohasekai.sagernet.fmt.socks.SOCKSBean
@@ -9,18 +11,48 @@ import io.nekohasekai.sagernet.fmt.v2ray.V2rayConfig.*
 import okhttp3.HttpUrl
 import okhttp3.HttpUrl.Companion.toHttpUrl
 
-fun buildV2rayConfig(proxy: ProxyEntity, listen: String, port: Int): V2rayConfig {
+const val TAG_SOCKS = "in"
+const val TAG_AGENT = "out"
+const val TAG_DIRECT = "bypass"
+const val TAG_DNS_IN = "dns-in"
+const val TAG_DNS_OUT = "dns-out"
+
+fun buildV2rayConfig(proxy: ProxyEntity): V2rayConfig {
+
+    val bind = if (DataStore.allowAccess) "0.0.0.0" else "127.0.0.1"
+    val remoteDns = DataStore.remoteDNS.split(",")
+    val domesticDns = DataStore.domesticDns.split(',')
 
     val bean = proxy.requireBean()
 
     return V2rayConfig().apply {
 
         dns = DnsObject().apply {
-            servers = listOf(
+            hosts = mapOf(
+                "domain:googleapis.cn" to "googleapis.com"
+            )
+            servers = mutableListOf()
+
+            servers.addAll(remoteDns.map {
                 DnsObject.StringOrServerObject().apply {
-                    valueX = "1.1.1.1"
+                    valueX = it
                 }
-            )
+            })
+
+            if (DataStore.enableLocalDNS) {
+                when (DataStore.routeMode) {
+                    RouteMode.BYPASS_LAN, RouteMode.BYPASS_LAN_CHINA -> {
+                        servers.add(DnsObject.StringOrServerObject().apply {
+                            valueY = DnsObject.ServerObject().apply {
+                                address = domesticDns.first()
+                                port = 53
+                                domains = listOf("geosite:cn")
+                                expectIPs = listOf("geoip:cn")
+                            }
+                        })
+                    }
+                }
+            }
         }
 
         log = LogObject().apply {
@@ -40,22 +72,24 @@ fun buildV2rayConfig(proxy: ProxyEntity, listen: String, port: Int): V2rayConfig
             }
         }
 
-        inbounds = listOf(
+        inbounds = mutableListOf()
+        inbounds.add(
             InboundObject().apply {
-                tag = "in"
-                this.listen = listen
-                this.port = port
+                tag = TAG_SOCKS
+                listen = bind
+                port = DataStore.socksPort
                 protocol = "socks"
                 settings = LazyInboundConfigurationObject(
                     SocksInboundConfigurationObject().apply {
                         auth = "noauth"
-                        udp = bean is SOCKSBean && bean.udp
+                        udp = true
                         userLevel = 0
                     })
             }
         )
 
-        outbounds = listOf(
+        outbounds = mutableListOf()
+        outbounds.add(
             OutboundObject().apply {
                 tag = "out"
                 if (bean is SOCKSBean) {
@@ -65,7 +99,7 @@ fun buildV2rayConfig(proxy: ProxyEntity, listen: String, port: Int): V2rayConfig
                             servers = listOf(
                                 SocksOutboundConfigurationObject.ServerObject().apply {
                                     address = bean.serverAddress
-                                    this.port = bean.serverPort
+                                    port = bean.serverPort
                                     if (!bean.username.isNullOrBlank()) {
                                         users =
                                             listOf(SocksOutboundConfigurationObject.ServerObject.UserObject()
@@ -83,12 +117,13 @@ fun buildV2rayConfig(proxy: ProxyEntity, listen: String, port: Int): V2rayConfig
                         settings = LazyOutboundConfigurationObject(
                             ShadowsocksOutboundConfigurationObject().apply {
                                 servers = listOf(
-                                    ShadowsocksOutboundConfigurationObject.ServerObject().apply {
-                                        address = bean.serverAddress
-                                        this.port = bean.serverPort
-                                        method = bean.method
-                                        password = bean.password
-                                    }
+                                    ShadowsocksOutboundConfigurationObject.ServerObject()
+                                        .apply {
+                                            address = bean.serverAddress
+                                            port = bean.serverPort
+                                            method = bean.method
+                                            password = bean.password
+                                        }
                                 )
                             })
                     } else {
@@ -98,26 +133,122 @@ fun buildV2rayConfig(proxy: ProxyEntity, listen: String, port: Int): V2rayConfig
                                 servers = listOf(
                                     SocksOutboundConfigurationObject.ServerObject().apply {
                                         address = "127.0.0.1"
-                                        this.port = port + 10
+                                        port = DataStore.socksPort + 10
                                     }
                                 )
                             })
                     }
                 }
-            },
+            }
+        )
+        outbounds.add(
             OutboundObject().apply {
-                tag = "direct"
+                tag = TAG_DIRECT
                 protocol = "freedom"
             }
         )
 
         routing = RoutingObject().apply {
             domainStrategy = "IPIfNonMatch"
-            rules = listOf(RoutingObject.RuleObject().apply {
-                inboundTag = listOf(
-                    "in"
-                )
-                outboundTag = "out"
+
+            rules = mutableListOf()
+
+            rules.add(RoutingObject.RuleObject().apply {
+                type = "field"
+                outboundTag = TAG_AGENT
+                domain = listOf("domain:googleapis.cn")
+            })
+
+            when (DataStore.routeMode) {
+                RouteMode.BYPASS_LAN -> {
+                    rules.add(RoutingObject.RuleObject().apply {
+                        type = "field"
+                        outboundTag = TAG_DIRECT
+                        ip = listOf("geoip:private")
+                    })
+                }
+                RouteMode.BYPASS_CHINA -> {
+                    rules.add(RoutingObject.RuleObject().apply {
+                        type = "field"
+                        outboundTag = TAG_DIRECT
+                        ip = listOf("geoip:cn")
+                    })
+                    rules.add(RoutingObject.RuleObject().apply {
+                        type = "field"
+                        outboundTag = TAG_DIRECT
+                        domain = listOf("geosite:cn")
+                    })
+                }
+                RouteMode.BYPASS_LAN_CHINA -> {
+                    rules.add(RoutingObject.RuleObject().apply {
+                        type = "field"
+                        outboundTag = TAG_DIRECT
+                        ip = listOf("geoip:private")
+                    })
+                    rules.add(RoutingObject.RuleObject().apply {
+                        type = "field"
+                        outboundTag = TAG_DIRECT
+                        ip = listOf("geoip:cn")
+                    })
+                    rules.add(RoutingObject.RuleObject().apply {
+                        type = "field"
+                        outboundTag = TAG_DIRECT
+                        domain = listOf("geosite:cn")
+                    })
+                }
+            }
+
+            rules.add(RoutingObject.RuleObject().apply {
+                inboundTag = listOf(TAG_SOCKS)
+                outboundTag = TAG_AGENT
+                type = "field"
+            })
+        }
+
+        if (DataStore.enableLocalDNS) {
+            inbounds.add(
+                InboundObject().apply {
+                    tag = TAG_DNS_IN
+                    listen = "127.0.0.1"
+                    port = DataStore.localDNSPort
+                    protocol = "dokodemo-door"
+                    settings = LazyInboundConfigurationObject(
+                        DokodemoDoorInboundConfigurationObject().apply {
+                            address = if (remoteDns.first().startsWith("https")) {
+                                "1.1.1.1"
+                            } else {
+                                remoteDns.first()
+                            }
+                            network = "tcp,udp"
+                            port = 53
+                        })
+                }
+            )
+            outbounds.add(
+                OutboundObject().apply {
+                    protocol = "dns"
+                    tag = TAG_DNS_OUT
+                }
+            )
+            if (!domesticDns.first().startsWith("https")) {
+                routing.rules.add(0, RoutingObject.RuleObject().apply {
+                    type = "field"
+                    outboundTag = TAG_DIRECT
+                    ip = listOf(domesticDns.first())
+                    port = "53"
+                })
+            }
+            if (!remoteDns.first().startsWith("https")) {
+                routing.rules.add(0, RoutingObject.RuleObject().apply {
+                    type = "field"
+                    outboundTag = TAG_AGENT
+                    ip = arrayListOf(remoteDns.first())
+                    port = "53"
+                })
+            }
+            routing.rules.add(0, RoutingObject.RuleObject().apply {
+                inboundTag = listOf(TAG_DNS_IN)
+                outboundTag = TAG_DNS_OUT
                 type = "field"
             })
         }
@@ -205,7 +336,8 @@ fun parseVmess1(link: String): VMessBean {
                 }
             }
             "kcp.uplinkcapacity" -> bean.kcpUpLinkCapacity = lnk.queryParameter(it)!!.toInt()
-            "kcp.downlinkcapacity" -> bean.kcpDownLinkCapacity = lnk.queryParameter(it)!!.toInt()
+            "kcp.downlinkcapacity" -> bean.kcpDownLinkCapacity =
+                lnk.queryParameter(it)!!.toInt()
             "header" -> bean.header = lnk.queryParameter(it)
             "mux" -> bean.mux = lnk.queryParameter(it)!!.toInt()
             // custom

+ 10 - 2
app/src/main/java/io/nekohasekai/sagernet/ktx/Preferences.kt

@@ -18,6 +18,14 @@ fun PreferenceDataStore.int(
     defaultValue: () -> Int = { 0 },
 ) = PreferenceProxy(name, defaultValue, ::getInt, ::putInt)
 
+fun PreferenceDataStore.stringToInt(
+    name: String,
+    defaultValue: () -> Int = { 0 },
+) = PreferenceProxy(name,
+    defaultValue,
+    { key, default -> getString(key, "$default")?.toInt() },
+    { key, value -> putString(key, "$value") })
+
 fun PreferenceDataStore.long(
     name: String,
     defaultValue: () -> Long = { 0L },
@@ -26,11 +34,11 @@ fun PreferenceDataStore.long(
 class PreferenceProxy<T>(
     val name: String,
     val defaultValue: () -> T,
-    val getter: (String, T) -> T,
+    val getter: (String, T) -> T?,
     val setter: (String, value: T) -> Unit,
 ) {
 
     operator fun setValue(thisObj: Any?, property: KProperty<*>, value: T) = setter(name, value)
-    operator fun getValue(thisObj: Any?, property: KProperty<*>) = getter(name, defaultValue())
+    operator fun getValue(thisObj: Any?, property: KProperty<*>) = getter(name, defaultValue())!!
 
 }

+ 15 - 3
app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt

@@ -26,12 +26,13 @@ import io.nekohasekai.sagernet.R
 import io.nekohasekai.sagernet.SagerNet
 import io.nekohasekai.sagernet.bg.BaseService
 import io.nekohasekai.sagernet.database.*
+import io.nekohasekai.sagernet.fmt.socks.SOCKSBean
 import io.nekohasekai.sagernet.fmt.socks.toUri
 import io.nekohasekai.sagernet.fmt.socks.toV2rayN
 import io.nekohasekai.sagernet.ktx.*
-import io.nekohasekai.sagernet.ui.settings.ProfileSettingsActivity
-import io.nekohasekai.sagernet.ui.settings.ShadowsocksSettingsActivity
-import io.nekohasekai.sagernet.ui.settings.SocksSettingsActivity
+import io.nekohasekai.sagernet.ui.profile.ProfileSettingsActivity
+import io.nekohasekai.sagernet.ui.profile.ShadowsocksSettingsActivity
+import io.nekohasekai.sagernet.ui.profile.SocksSettingsActivity
 import io.nekohasekai.sagernet.widget.QRCodeDialog
 import io.nekohasekai.sagernet.widget.UndoSnackbarManager
 import java.util.*
@@ -343,6 +344,16 @@ class ConfigurationFragment : ToolbarFragment(R.layout.group_list_main),
                 runOnDefaultDispatcher {
                     configurationList.clear()
                     configurationList.addAll(SagerDatabase.proxyDao.getByGroup(proxyGroup.id))
+                    if (configurationList.isEmpty() && proxyGroup.isDefault) {
+                        configurationList.add(ProfileManager.createProfile(groupId,
+                            SOCKSBean.DEFAULT_BEAN.clone().apply {
+                                name = "Local tunnel"
+                            }))
+                        if (DataStore.selectedProxy == 0L) {
+                            DataStore.selectedProxy = configurationList[0].id
+                        }
+                    }
+
                     onMainDispatcher {
                         notifyDataSetChanged()
                     }
@@ -362,6 +373,7 @@ class ConfigurationFragment : ToolbarFragment(R.layout.group_list_main),
         override fun onDestroyView() {
             super.onDestroyView()
 
+            if (!::undoManager.isInitialized) return
             undoManager.flush()
         }
 

+ 5 - 0
app/src/main/java/io/nekohasekai/sagernet/ui/MainActivity.kt

@@ -33,6 +33,10 @@ import io.nekohasekai.sagernet.widget.StatsBar
 class MainActivity : AppCompatActivity(), SagerConnection.Callback,
     OnPreferenceDataStoreChangeListener {
 
+    companion object {
+        var stateListener: ((BaseService.State) -> Unit)? = null
+    }
+
     private lateinit var appBarConfiguration: AppBarConfiguration
     lateinit var fab: ServiceButton
     lateinit var stats: StatsBar
@@ -86,6 +90,7 @@ class MainActivity : AppCompatActivity(), SagerConnection.Callback,
         stats.changeState(state)
         if (msg != null) snackbar(getString(R.string.vpn_error, msg)).show()
         this.state = state
+        stateListener?.invoke(state)
     }
 
     fun snackbar(text: CharSequence = ""): Snackbar {

+ 22 - 0
app/src/main/java/io/nekohasekai/sagernet/ui/SettingsFragment.kt

@@ -0,0 +1,22 @@
+package io.nekohasekai.sagernet.ui
+
+import android.os.Bundle
+import android.view.View
+import androidx.core.view.ViewCompat
+import io.nekohasekai.sagernet.R
+import io.nekohasekai.sagernet.widget.ListHolderListener
+
+class SettingsFragment : ToolbarFragment(R.layout.settings_activity) {
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+
+        ViewCompat.setOnApplyWindowInsetsListener(view, ListHolderListener)
+        toolbar.setTitle(R.string.settings)
+
+        parentFragmentManager.beginTransaction()
+            .replace(R.id.settings, SettingsPreferenceFragment())
+            .commit()
+    }
+
+}

+ 82 - 0
app/src/main/java/io/nekohasekai/sagernet/ui/SettingsPreferenceFragment.kt

@@ -0,0 +1,82 @@
+package io.nekohasekai.sagernet.ui
+
+import android.os.Bundle
+import androidx.preference.EditTextPreference
+import androidx.preference.Preference
+import androidx.preference.PreferenceFragmentCompat
+import androidx.preference.SwitchPreference
+import io.nekohasekai.sagernet.Key
+import io.nekohasekai.sagernet.R
+import io.nekohasekai.sagernet.RouteMode
+import io.nekohasekai.sagernet.bg.BaseService
+import io.nekohasekai.sagernet.database.DataStore
+import io.nekohasekai.sagernet.database.preference.EditTextPreferenceModifiers
+
+class SettingsPreferenceFragment : PreferenceFragmentCompat() {
+
+    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+        preferenceManager.preferenceDataStore = DataStore.configurationStore
+        DataStore.initGlobal()
+        addPreferencesFromResource(R.xml.global_preferences)
+
+        val serviceMode = findPreference<Preference>(Key.SERVICE_MODE)!!
+        val routeMode = findPreference<Preference>(Key.ROUTE_MODE)!!
+        val allowAccess = findPreference<Preference>(Key.ALLOW_ACCESS)!!
+        val ipv6Route = findPreference<Preference>(Key.IPV6_ROUTE)!!
+        val preferIpv6 = findPreference<Preference>(Key.PREFER_IPV6)!!
+
+        val remoteDns = findPreference<Preference>(Key.REMOTE_DNS)!!
+        val enableLocalDns = findPreference<SwitchPreference>(Key.ENABLE_LOCAL_DNS)!!
+        val portLocalDns = findPreference<EditTextPreference>(Key.LOCAL_DNS_PORT)!!
+        val domesticDns = findPreference<EditTextPreference>(Key.DOMESTIC_DNS)!!
+        portLocalDns.setOnBindEditTextListener(EditTextPreferenceModifiers.Port)
+
+        val onRouteChange = Preference.OnPreferenceChangeListener { _, newValue ->
+            when (newValue) {
+                RouteMode.BYPASS_CHINA, RouteMode.BYPASS_LAN_CHINA -> {
+                    enableLocalDns.isChecked = true
+                    enableLocalDns.isEnabled = false
+                    DataStore.enableLocalDNS = true
+                }
+                else -> {
+                    enableLocalDns.isEnabled = true
+                }
+            }
+            true
+        }
+        val listener: (BaseService.State) -> Unit = {
+            val stopped = it == BaseService.State.Stopped
+            serviceMode.isEnabled = stopped
+            routeMode.isEnabled = stopped
+            allowAccess.isEnabled = stopped
+            remoteDns.isEnabled = stopped
+            portLocalDns.isEnabled = stopped
+            domesticDns.isEnabled = stopped
+            ipv6Route.isEnabled = stopped
+            preferIpv6.isEnabled = stopped
+
+            when (DataStore.routeMode) {
+                RouteMode.BYPASS_CHINA, RouteMode.BYPASS_LAN_CHINA -> {
+                    enableLocalDns.isChecked = true
+                    enableLocalDns.isEnabled = false
+                }
+                else -> {
+                    enableLocalDns.isEnabled = stopped
+                }
+            }
+
+            //  portProxy.isEnabled = stopped
+            /*   if (stopped) onServiceModeChange.onPreferenceChange(null, DataStore.serviceMode) else {
+                   portTransproxy.isEnabled = false
+               }*/
+        }
+        listener((activity as MainActivity).state)
+        MainActivity.stateListener = listener
+        routeMode.onPreferenceChangeListener = onRouteChange
+    }
+
+    override fun onDestroy() {
+        MainActivity.stateListener = null
+        super.onDestroy()
+    }
+}

+ 1 - 1
app/src/main/java/io/nekohasekai/sagernet/ui/settings/ProfileSettingsActivity.kt → app/src/main/java/io/nekohasekai/sagernet/ui/profile/ProfileSettingsActivity.kt

@@ -1,4 +1,4 @@
-package io.nekohasekai.sagernet.ui.settings
+package io.nekohasekai.sagernet.ui.profile
 
 import android.content.DialogInterface
 import android.os.Bundle

+ 1 - 1
app/src/main/java/io/nekohasekai/sagernet/ui/settings/ShadowsocksSettingsActivity.kt → app/src/main/java/io/nekohasekai/sagernet/ui/profile/ShadowsocksSettingsActivity.kt

@@ -1,4 +1,4 @@
-package io.nekohasekai.sagernet.ui.settings
+package io.nekohasekai.sagernet.ui.profile
 
 import android.app.Activity
 import android.content.BroadcastReceiver

+ 1 - 1
app/src/main/java/io/nekohasekai/sagernet/ui/settings/SocksSettingsActivity.kt → app/src/main/java/io/nekohasekai/sagernet/ui/profile/SocksSettingsActivity.kt

@@ -1,4 +1,4 @@
-package io.nekohasekai.sagernet.ui.settings
+package io.nekohasekai.sagernet.ui.profile
 
 import android.os.Bundle
 import androidx.preference.EditTextPreference

+ 1 - 1
app/src/main/java/io/nekohasekai/sagernet/widget/QRCodeDialog.kt

@@ -21,7 +21,7 @@ import java.nio.charset.StandardCharsets
 class QRCodeDialog() : DialogFragment() {
 
     companion object {
-        private const val KEY_URL = "com.github.shadowsocks.QRCodeDialog.KEY_URL"
+        private const val KEY_URL = "io.nekohasekai.sagernet.QRCodeDialog.KEY_URL"
         private val iso88591 = StandardCharsets.ISO_8859_1.newEncoder()
     }
 

+ 13 - 0
app/src/main/res/drawable/ic_baseline_nat_24.xml

@@ -0,0 +1,13 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M6.82,13H11v-2H6.82C6.4,9.84 5.3,9 4,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3C5.3,15 6.4,14.16 6.82,13zM4,13c-0.55,0 -1,-0.45 -1,-1c0,-0.55 0.45,-1 1,-1s1,0.45 1,1C5,12.55 4.55,13 4,13z"/>
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M23,12l-4,-3v2h-4.05C14.45,5.95 10.19,2 5,2v2c4.42,0 8,3.58 8,8s-3.58,8 -8,8v2c5.19,0 9.45,-3.95 9.95,-9H19v2L23,12z"/>
+</vector>

+ 6 - 1
app/src/main/res/menu/activity_main_drawer.xml

@@ -8,10 +8,15 @@
             android:id="@+id/nav_configuration"
             android:icon="@drawable/ic_action_description"
             android:title="@string/menu_configuration" />
+
+        <item
+            android:id="@+id/nav_settings"
+            android:icon="@drawable/ic_action_settings"
+            android:title="@string/settings" />
         <item
             android:id="@+id/nav_about"
             android:icon="@drawable/ic_baseline_info_24"
             android:title="@string/menu_about" />
     </group>
-    
+
 </menu>

+ 6 - 0
app/src/main/res/navigation/mobile_navigation.xml

@@ -11,6 +11,12 @@
         android:label="@string/menu_configuration"
         tools:layout="@layout/group_list_main" />
 
+    <fragment
+        android:id="@+id/nav_settings"
+        android:name="io.nekohasekai.sagernet.ui.SettingsFragment"
+        android:label="@string/settings"
+        tools:layout="@layout/settings_activity" />
+
     <fragment
         android:id="@+id/nav_about"
         android:name="io.nekohasekai.sagernet.ui.AboutFragment"

+ 187 - 0
app/src/main/res/values/arrays.xml

@@ -1,5 +1,163 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
+    <string-array name="bypass_private_route" translatable="false">
+        <item>1.0.0.0/8</item>
+        <item>2.0.0.0/7</item>
+        <item>4.0.0.0/6</item>
+        <item>8.0.0.0/7</item>
+        <item>11.0.0.0/8</item>
+        <item>12.0.0.0/6</item>
+        <item>16.0.0.0/4</item>
+        <item>32.0.0.0/3</item>
+        <item>64.0.0.0/3</item>
+        <item>96.0.0.0/6</item>
+        <item>100.0.0.0/10</item>
+        <item>100.128.0.0/9</item>
+        <item>101.0.0.0/8</item>
+        <item>102.0.0.0/7</item>
+        <item>104.0.0.0/5</item>
+        <item>112.0.0.0/10</item>
+        <item>112.64.0.0/11</item>
+        <item>112.96.0.0/12</item>
+        <item>112.112.0.0/13</item>
+        <item>112.120.0.0/14</item>
+        <item>112.124.0.0/19</item>
+        <item>112.124.32.0/21</item>
+        <item>112.124.40.0/22</item>
+        <item>112.124.44.0/23</item>
+        <item>112.124.46.0/24</item>
+        <item>112.124.48.0/20</item>
+        <item>112.124.64.0/18</item>
+        <item>112.124.128.0/17</item>
+        <item>112.125.0.0/16</item>
+        <item>112.126.0.0/15</item>
+        <item>112.128.0.0/9</item>
+        <item>113.0.0.0/8</item>
+        <item>114.0.0.0/10</item>
+        <item>114.64.0.0/11</item>
+        <item>114.96.0.0/12</item>
+        <item>114.112.0.0/15</item>
+        <item>114.114.0.0/18</item>
+        <item>114.114.64.0/19</item>
+        <item>114.114.96.0/20</item>
+        <item>114.114.112.0/23</item>
+        <item>114.114.115.0/24</item>
+        <item>114.114.116.0/22</item>
+        <item>114.114.120.0/21</item>
+        <item>114.114.128.0/17</item>
+        <item>114.115.0.0/16</item>
+        <item>114.116.0.0/14</item>
+        <item>114.120.0.0/13</item>
+        <item>114.128.0.0/9</item>
+        <item>115.0.0.0/8</item>
+        <item>116.0.0.0/6</item>
+        <item>120.0.0.0/6</item>
+        <item>124.0.0.0/7</item>
+        <item>126.0.0.0/8</item>
+        <item>128.0.0.0/3</item>
+        <item>160.0.0.0/5</item>
+        <item>168.0.0.0/8</item>
+        <item>169.0.0.0/9</item>
+        <item>169.128.0.0/10</item>
+        <item>169.192.0.0/11</item>
+        <item>169.224.0.0/12</item>
+        <item>169.240.0.0/13</item>
+        <item>169.248.0.0/14</item>
+        <item>169.252.0.0/15</item>
+        <item>169.255.0.0/16</item>
+        <item>170.0.0.0/7</item>
+        <item>172.0.0.0/12</item>
+        <item>172.32.0.0/11</item>
+        <item>172.64.0.0/10</item>
+        <item>172.128.0.0/9</item>
+        <item>173.0.0.0/8</item>
+        <item>174.0.0.0/7</item>
+        <item>176.0.0.0/4</item>
+        <item>192.0.0.8/29</item>
+        <item>192.0.0.16/28</item>
+        <item>192.0.0.32/27</item>
+        <item>192.0.0.64/26</item>
+        <item>192.0.0.128/25</item>
+        <item>192.0.1.0/24</item>
+        <item>192.0.3.0/24</item>
+        <item>192.0.4.0/22</item>
+        <item>192.0.8.0/21</item>
+        <item>192.0.16.0/20</item>
+        <item>192.0.32.0/19</item>
+        <item>192.0.64.0/18</item>
+        <item>192.0.128.0/17</item>
+        <item>192.1.0.0/16</item>
+        <item>192.2.0.0/15</item>
+        <item>192.4.0.0/14</item>
+        <item>192.8.0.0/13</item>
+        <item>192.16.0.0/12</item>
+        <item>192.32.0.0/11</item>
+        <item>192.64.0.0/12</item>
+        <item>192.80.0.0/13</item>
+        <item>192.88.0.0/18</item>
+        <item>192.88.64.0/19</item>
+        <item>192.88.96.0/23</item>
+        <item>192.88.98.0/24</item>
+        <item>192.88.100.0/22</item>
+        <item>192.88.104.0/21</item>
+        <item>192.88.112.0/20</item>
+        <item>192.88.128.0/17</item>
+        <item>192.89.0.0/16</item>
+        <item>192.90.0.0/15</item>
+        <item>192.92.0.0/14</item>
+        <item>192.96.0.0/11</item>
+        <item>192.128.0.0/11</item>
+        <item>192.160.0.0/13</item>
+        <item>192.169.0.0/16</item>
+        <item>192.170.0.0/15</item>
+        <item>192.172.0.0/14</item>
+        <item>192.176.0.0/12</item>
+        <item>192.192.0.0/10</item>
+        <item>193.0.0.0/8</item>
+        <item>194.0.0.0/7</item>
+        <item>196.0.0.0/7</item>
+        <item>198.0.0.0/12</item>
+        <item>198.16.0.0/15</item>
+        <item>198.20.0.0/14</item>
+        <item>198.24.0.0/13</item>
+        <item>198.32.0.0/12</item>
+        <item>198.48.0.0/15</item>
+        <item>198.50.0.0/16</item>
+        <item>198.51.0.0/18</item>
+        <item>198.51.64.0/19</item>
+        <item>198.51.96.0/22</item>
+        <item>198.51.101.0/24</item>
+        <item>198.51.102.0/23</item>
+        <item>198.51.104.0/21</item>
+        <item>198.51.112.0/20</item>
+        <item>198.51.128.0/17</item>
+        <item>198.52.0.0/14</item>
+        <item>198.56.0.0/13</item>
+        <item>198.64.0.0/10</item>
+        <item>198.128.0.0/9</item>
+        <item>199.0.0.0/8</item>
+        <item>200.0.0.0/7</item>
+        <item>202.0.0.0/8</item>
+        <item>203.0.0.0/18</item>
+        <item>203.0.64.0/19</item>
+        <item>203.0.96.0/20</item>
+        <item>203.0.112.0/24</item>
+        <item>203.0.114.0/23</item>
+        <item>203.0.116.0/22</item>
+        <item>203.0.120.0/21</item>
+        <item>203.0.128.0/17</item>
+        <item>203.1.0.0/16</item>
+        <item>203.2.0.0/15</item>
+        <item>203.4.0.0/14</item>
+        <item>203.8.0.0/13</item>
+        <item>203.16.0.0/12</item>
+        <item>203.32.0.0/11</item>
+        <item>203.64.0.0/10</item>
+        <item>203.128.0.0/9</item>
+        <item>204.0.0.0/6</item>
+        <item>208.0.0.0/4</item>
+    </string-array>
+
     <string-array name="enc_method_entry" translatable="false">
         <item>NONE</item>
         <item>RC4-MD5</item>
@@ -39,4 +197,33 @@
         <item>chacha20-ietf-poly1305</item>
         <item>xchacha20-ietf-poly1305</item>
     </string-array>
+
+    <string-array name="service_modes">
+        <item>@string/service_mode_vpn</item>
+        <item>@string/service_mode_proxy</item>
+<!--
+        <item>@string/service_mode_transproxy</item>
+-->
+    </string-array>
+    <string-array name="service_mode_values" translatable="false">
+        <item>vpn</item>
+        <item>proxy</item>
+<!--
+        <item>transproxy</item>
+-->
+    </string-array>
+
+    <string-array name="route_entry">
+        <item>@string/route_entry_all</item>
+        <item>@string/route_entry_bypass_lan</item>
+        <item>@string/route_entry_bypass_chn</item>
+        <item>@string/route_entry_bypass_lan_chn</item>
+    </string-array>
+    <string-array name="route_value" translatable="false">
+        <item>all</item>
+        <item>bypass-lan</item>
+        <item>bypass-china</item>
+        <item>bypass-lan-china</item>
+    </string-array>
+
 </resources>

+ 10 - 1
app/src/main/res/values/strings.xml

@@ -28,6 +28,12 @@
     <string name="port_proxy">SOCKS5 proxy port</string>
     <string name="port_local_dns">Local DNS port</string>
     <string name="port_transproxy">Transproxy port</string>
+    <string name="allow_access">Allow connections from the LAN</string>
+    <string name="allow_access_sum">Bind inbound servers to 0.0.0.0</string>
+    <string name="cag_dns">DNS Settings</string>
+    <string name="local_dns">Enable local DNS</string>
+    <string name="domestic_dns">Domestic DNS</string>
+    <string name="cag_route">Route Settings</string>
 
     <string name="remote_dns">Remote DNS</string>
     <string name="traffic">%1$s↑\t%2$s↓</string>
@@ -54,6 +60,9 @@
     <!-- feature category -->
     <string name="ipv6">IPv6 Route</string>
     <string name="ipv6_summary">Redirect IPv6 traffic to remote</string>
+    <string name="ipv6_prefer">Prefer IPv6</string>
+    <string name="ipv6_prefer_summary">Prefer IPv6 address and routes</string>
+
     <string name="metered">Metered Hint</string>
     <string name="metered_summary">Hint system to treat VPN as metered</string>
     <string name="route_list">Route</string>
@@ -209,6 +218,6 @@
     <string name="no">No</string>
     <string name="apply">Apply</string>
 
-    <string name="about_text">The universal proxy toolchain for Android, written in Kotlin.\n\nThis program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.\n\This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/.</string>
+    <string name="about_text">The universal proxy toolchain for Android, written in Kotlin.\n\nThis program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.\n\nThis program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/.</string>
 
 </resources>

+ 86 - 0
app/src/main/res/xml/global_preferences.xml

@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="utf-8"?>
+<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
+
+    <com.takisoft.preferencex.SimpleMenuPreference
+        app:defaultValue="vpn"
+        app:entries="@array/service_modes"
+        app:entryValues="@array/service_mode_values"
+        app:icon="@drawable/ic_device_developer_mode"
+        app:key="serviceMode"
+        app:title="@string/service_mode"
+        app:useSimpleSummaryProvider="true" />
+
+    <PreferenceCategory app:title="@string/cag_route">
+        <com.takisoft.preferencex.SimpleMenuPreference
+            app:defaultValue="bypass-lan-china"
+            app:entries="@array/route_entry"
+            app:entryValues="@array/route_value"
+            app:icon="@drawable/ic_maps_directions"
+            app:key="routeMode"
+            app:title="@string/route_list"
+            app:useSimpleSummaryProvider="true" />
+        <SwitchPreference
+            app:icon="@drawable/ic_image_looks_6"
+            app:key="ipv6Route"
+            app:summary="@string/ipv6_summary"
+            app:title="@string/ipv6"
+            app:useSimpleSummaryProvider="true" />
+        <SwitchPreference
+            app:key="preferIpv6"
+            app:summary="@string/ipv6_prefer_summary"
+            app:title="@string/ipv6_prefer" />
+        <SwitchPreference
+            app:icon="@drawable/ic_baseline_nat_24"
+            app:key="allowAccess"
+            app:summary="@string/allow_access_sum"
+            app:title="@string/allow_access" />
+    </PreferenceCategory>
+
+    <!-- <SwitchPreference
+         app:icon="@drawable/ic_communication_phonelink_ring"
+         app:key="isAutoConnect"
+         app:summary="@string/auto_connect_summary"
+         app:title="@string/auto_connect" />
+
+     <SwitchPreference
+         app:icon="@drawable/ic_action_lock"
+         app:key="directBootAware"
+         app:summary="@string/direct_boot_aware_summary"
+         app:title="@string/direct_boot_aware" />-->
+
+    <!--<EditTextPreference
+        app:icon="@drawable/ic_maps_directions_boat"
+        app:key="portProxy"
+        app:title="@string/port_proxy"
+        app:useSimpleSummaryProvider="true" />-->
+
+    <PreferenceCategory app:title="@string/cag_dns">
+        <EditTextPreference
+            app:defaultValue="1.1.1.1"
+            app:icon="@drawable/ic_action_dns"
+            app:key="remoteDns"
+            app:title="@string/remote_dns"
+            app:useSimpleSummaryProvider="true" />
+        <SwitchPreference
+            app:key="enableLocalDns"
+            app:title="@string/local_dns" />
+        <EditTextPreference
+            app:defaultValue="5450"
+            app:key="portLocalDns"
+            app:title="@string/port_local_dns"
+            app:useSimpleSummaryProvider="true" />
+        <EditTextPreference
+            app:defaultValue="223.5.5.5"
+            app:key="domesticDns"
+            app:title="@string/domestic_dns"
+            app:useSimpleSummaryProvider="true" />
+    </PreferenceCategory>
+
+    <!--  <EditTextPreference
+          app:isPreferenceVisible="false"
+          app:key="portTransproxy"
+          app:title="@string/port_transproxy"
+          app:useSimpleSummaryProvider="true" />-->
+
+
+</PreferenceScreen>

+ 4 - 0
bin/update_geofile.sh

@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+
+curl -L -o app/src/main/assets/geofile/geoip.dat "https://github.com/v2fly/geoip/raw/release/geoip.dat"
+curl -L -o app/src/main/assets/geofile/geosite.dat "https://github.com/v2fly/domain-list-community/raw/release/dlc.dat"

+ 1 - 1
v2ray

@@ -1 +1 @@
-Subproject commit a697372e996000fa748f87b4c2bad4da038b5ddf
+Subproject commit e93dded26a3fe32412dac972781a749862ead664