feat!: dual-stack kill switch support, metered tunnels

Adds dual-stack option for kill switch.

Add metered option for kill switch and individual tunnels.

closes #966
closes #962
This commit is contained in:
Zane Schepke
2025-10-28 21:36:07 -04:00
parent 59a70e53ff
commit b4b96a7e77
52 changed files with 1899 additions and 480 deletions
@@ -0,0 +1,509 @@
{
"formatVersion": 1,
"database": {
"version": 26,
"identityHash": "a420594a08fff58ecda3e0424fb43e47",
"entities": [
{
"tableName": "tunnel_config",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false, `am_quick` TEXT NOT NULL DEFAULT '', `is_Active` INTEGER NOT NULL DEFAULT false, `restart_on_ping_failure` INTEGER NOT NULL DEFAULT false, `ping_target` TEXT DEFAULT null, `is_ethernet_tunnel` INTEGER NOT NULL DEFAULT false, `is_ipv4_preferred` INTEGER NOT NULL DEFAULT true, `position` INTEGER NOT NULL DEFAULT 0, `auto_tunnel_apps` TEXT NOT NULL DEFAULT '[]')",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "wgQuick",
"columnName": "wg_quick",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tunnelNetworks",
"columnName": "tunnel_networks",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isMobileDataTunnel",
"columnName": "is_mobile_data_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPrimaryTunnel",
"columnName": "is_primary_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "amQuick",
"columnName": "am_quick",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isActive",
"columnName": "is_Active",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "restartOnPingFailure",
"columnName": "restart_on_ping_failure",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "pingTarget",
"columnName": "ping_target",
"affinity": "TEXT",
"defaultValue": "null"
},
{
"fieldPath": "isEthernetTunnel",
"columnName": "is_ethernet_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isIpv4Preferred",
"columnName": "is_ipv4_preferred",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "true"
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "autoTunnelApps",
"columnName": "auto_tunnel_apps",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'[]'"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_tunnel_config_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_tunnel_config_name` ON `${TABLE_NAME}` (`name`)"
}
]
},
{
"tableName": "proxy_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `socks5_proxy_enabled` INTEGER NOT NULL DEFAULT 0, `socks5_proxy_bind_address` TEXT, `http_proxy_enable` INTEGER NOT NULL DEFAULT 0, `http_proxy_bind_address` TEXT, `proxy_username` TEXT, `proxy_password` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "socks5ProxyEnabled",
"columnName": "socks5_proxy_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "socks5ProxyBindAddress",
"columnName": "socks5_proxy_bind_address",
"affinity": "TEXT"
},
{
"fieldPath": "httpProxyEnabled",
"columnName": "http_proxy_enable",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "httpProxyBindAddress",
"columnName": "http_proxy_bind_address",
"affinity": "TEXT"
},
{
"fieldPath": "proxyUsername",
"columnName": "proxy_username",
"affinity": "TEXT"
},
{
"fieldPath": "proxyPassword",
"columnName": "proxy_password",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "general_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT 0, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT 0, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_globals_enabled` INTEGER NOT NULL DEFAULT 0, `app_mode` INTEGER NOT NULL DEFAULT 0, `theme` TEXT NOT NULL DEFAULT 'AUTOMATIC', `locale` TEXT, `remote_key` TEXT, `is_remote_control_enabled` INTEGER NOT NULL DEFAULT 0, `is_pin_lock_enabled` INTEGER NOT NULL DEFAULT 0, `is_always_on_vpn_enabled` INTEGER NOT NULL DEFAULT 0, `custom_split_packages` TEXT NOT NULL DEFAULT '{}')",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isShortcutsEnabled",
"columnName": "is_shortcuts_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isRestoreOnBootEnabled",
"columnName": "is_restore_on_boot_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isMultiTunnelEnabled",
"columnName": "is_multi_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isTunnelGlobalsEnabled",
"columnName": "is_tunnel_globals_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "appMode",
"columnName": "app_mode",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "theme",
"columnName": "theme",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'AUTOMATIC'"
},
{
"fieldPath": "locale",
"columnName": "locale",
"affinity": "TEXT"
},
{
"fieldPath": "remoteKey",
"columnName": "remote_key",
"affinity": "TEXT"
},
{
"fieldPath": "isRemoteControlEnabled",
"columnName": "is_remote_control_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isPinLockEnabled",
"columnName": "is_pin_lock_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isAlwaysOnVpnEnabled",
"columnName": "is_always_on_vpn_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "customSplitPackages",
"columnName": "custom_split_packages",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'{}'"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "auto_tunnel_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL DEFAULT 0, `trusted_network_ssids` TEXT NOT NULL DEFAULT '', `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT 0, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT 0, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT 0, `debounce_delay_seconds` INTEGER NOT NULL DEFAULT 3, `is_tunnel_on_unsecure_enabled` INTEGER NOT NULL DEFAULT 0, `wifi_detection_method` INTEGER NOT NULL DEFAULT 0, `start_on_boot` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isAutoTunnelEnabled",
"columnName": "is_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isTunnelOnMobileDataEnabled",
"columnName": "is_tunnel_on_mobile_data_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "trustedNetworkSSIDs",
"columnName": "trusted_network_ssids",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isTunnelOnEthernetEnabled",
"columnName": "is_tunnel_on_ethernet_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isTunnelOnWifiEnabled",
"columnName": "is_tunnel_on_wifi_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isWildcardsEnabled",
"columnName": "is_wildcards_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isStopOnNoInternetEnabled",
"columnName": "is_stop_on_no_internet_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "debounceDelaySeconds",
"columnName": "debounce_delay_seconds",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "3"
},
{
"fieldPath": "isTunnelOnUnsecureEnabled",
"columnName": "is_tunnel_on_unsecure_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "wifiDetectionMethod",
"columnName": "wifi_detection_method",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "startOnBoot",
"columnName": "start_on_boot",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "monitoring_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_ping_enabled` INTEGER NOT NULL DEFAULT 0, `is_ping_monitoring_enabled` INTEGER NOT NULL DEFAULT 1, `tunnel_ping_interval_sec` INTEGER NOT NULL DEFAULT 30, `tunnel_ping_attempts` INTEGER NOT NULL DEFAULT 3, `tunnel_ping_timeout_sec` INTEGER, `show_detailed_ping_stats` INTEGER NOT NULL DEFAULT 0, `is_local_logs_enabled` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isPingMonitoringEnabled",
"columnName": "is_ping_monitoring_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "1"
},
{
"fieldPath": "tunnelPingIntervalSeconds",
"columnName": "tunnel_ping_interval_sec",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "30"
},
{
"fieldPath": "tunnelPingAttempts",
"columnName": "tunnel_ping_attempts",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "3"
},
{
"fieldPath": "tunnelPingTimeoutSeconds",
"columnName": "tunnel_ping_timeout_sec",
"affinity": "INTEGER"
},
{
"fieldPath": "showDetailedPingStats",
"columnName": "show_detailed_ping_stats",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isLocalLogsEnabled",
"columnName": "is_local_logs_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "dns_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `dns_protocol` INTEGER NOT NULL DEFAULT 0, `dns_endpoint` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dnsProtocol",
"columnName": "dns_protocol",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "dnsEndpoint",
"columnName": "dns_endpoint",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "lockdown_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `bypass_lan` INTEGER NOT NULL DEFAULT 0, `metered` INTEGER NOT NULL DEFAULT 0, `dual_stack` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "bypassLan",
"columnName": "bypass_lan",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "metered",
"columnName": "metered",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "dualStack",
"columnName": "dual_stack",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a420594a08fff58ecda3e0424fb43e47')"
]
}
}
@@ -0,0 +1,523 @@
{
"formatVersion": 1,
"database": {
"version": 27,
"identityHash": "98452d8160a1ae66c852ec8cd739e675",
"entities": [
{
"tableName": "tunnel_config",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false, `am_quick` TEXT NOT NULL DEFAULT '', `is_Active` INTEGER NOT NULL DEFAULT false, `restart_on_ping_failure` INTEGER NOT NULL DEFAULT false, `ping_target` TEXT DEFAULT null, `is_ethernet_tunnel` INTEGER NOT NULL DEFAULT false, `is_ipv4_preferred` INTEGER NOT NULL DEFAULT true, `position` INTEGER NOT NULL DEFAULT 0, `auto_tunnel_apps` TEXT NOT NULL DEFAULT '[]', `is_metered` INTEGER NOT NULL DEFAULT true)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "wgQuick",
"columnName": "wg_quick",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tunnelNetworks",
"columnName": "tunnel_networks",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isMobileDataTunnel",
"columnName": "is_mobile_data_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPrimaryTunnel",
"columnName": "is_primary_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "amQuick",
"columnName": "am_quick",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isActive",
"columnName": "is_Active",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "restartOnPingFailure",
"columnName": "restart_on_ping_failure",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "pingTarget",
"columnName": "ping_target",
"affinity": "TEXT",
"defaultValue": "null"
},
{
"fieldPath": "isEthernetTunnel",
"columnName": "is_ethernet_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isIpv4Preferred",
"columnName": "is_ipv4_preferred",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "true"
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "autoTunnelApps",
"columnName": "auto_tunnel_apps",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'[]'"
},
{
"fieldPath": "isMetered",
"columnName": "is_metered",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "true"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_tunnel_config_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_tunnel_config_name` ON `${TABLE_NAME}` (`name`)"
}
]
},
{
"tableName": "proxy_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `socks5_proxy_enabled` INTEGER NOT NULL DEFAULT 0, `socks5_proxy_bind_address` TEXT, `http_proxy_enable` INTEGER NOT NULL DEFAULT 0, `http_proxy_bind_address` TEXT, `proxy_username` TEXT, `proxy_password` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "socks5ProxyEnabled",
"columnName": "socks5_proxy_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "socks5ProxyBindAddress",
"columnName": "socks5_proxy_bind_address",
"affinity": "TEXT"
},
{
"fieldPath": "httpProxyEnabled",
"columnName": "http_proxy_enable",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "httpProxyBindAddress",
"columnName": "http_proxy_bind_address",
"affinity": "TEXT"
},
{
"fieldPath": "proxyUsername",
"columnName": "proxy_username",
"affinity": "TEXT"
},
{
"fieldPath": "proxyPassword",
"columnName": "proxy_password",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "general_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT 0, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT 0, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `global_split_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `app_mode` INTEGER NOT NULL DEFAULT 0, `theme` TEXT NOT NULL DEFAULT 'AUTOMATIC', `locale` TEXT, `remote_key` TEXT, `is_remote_control_enabled` INTEGER NOT NULL DEFAULT 0, `is_pin_lock_enabled` INTEGER NOT NULL DEFAULT 0, `is_always_on_vpn_enabled` INTEGER NOT NULL DEFAULT 0, `custom_split_packages` TEXT NOT NULL DEFAULT '{}')",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isShortcutsEnabled",
"columnName": "is_shortcuts_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isRestoreOnBootEnabled",
"columnName": "is_restore_on_boot_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isMultiTunnelEnabled",
"columnName": "is_multi_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isGlobalSplitTunnelEnabled",
"columnName": "global_split_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "appMode",
"columnName": "app_mode",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "theme",
"columnName": "theme",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'AUTOMATIC'"
},
{
"fieldPath": "locale",
"columnName": "locale",
"affinity": "TEXT"
},
{
"fieldPath": "remoteKey",
"columnName": "remote_key",
"affinity": "TEXT"
},
{
"fieldPath": "isRemoteControlEnabled",
"columnName": "is_remote_control_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isPinLockEnabled",
"columnName": "is_pin_lock_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isAlwaysOnVpnEnabled",
"columnName": "is_always_on_vpn_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "customSplitPackages",
"columnName": "custom_split_packages",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'{}'"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "auto_tunnel_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL DEFAULT 0, `trusted_network_ssids` TEXT NOT NULL DEFAULT '', `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT 0, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT 0, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT 0, `debounce_delay_seconds` INTEGER NOT NULL DEFAULT 3, `is_tunnel_on_unsecure_enabled` INTEGER NOT NULL DEFAULT 0, `wifi_detection_method` INTEGER NOT NULL DEFAULT 0, `start_on_boot` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isAutoTunnelEnabled",
"columnName": "is_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isTunnelOnMobileDataEnabled",
"columnName": "is_tunnel_on_mobile_data_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "trustedNetworkSSIDs",
"columnName": "trusted_network_ssids",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isTunnelOnEthernetEnabled",
"columnName": "is_tunnel_on_ethernet_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isTunnelOnWifiEnabled",
"columnName": "is_tunnel_on_wifi_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isWildcardsEnabled",
"columnName": "is_wildcards_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isStopOnNoInternetEnabled",
"columnName": "is_stop_on_no_internet_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "debounceDelaySeconds",
"columnName": "debounce_delay_seconds",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "3"
},
{
"fieldPath": "isTunnelOnUnsecureEnabled",
"columnName": "is_tunnel_on_unsecure_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "wifiDetectionMethod",
"columnName": "wifi_detection_method",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "startOnBoot",
"columnName": "start_on_boot",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "monitoring_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_ping_enabled` INTEGER NOT NULL DEFAULT 0, `is_ping_monitoring_enabled` INTEGER NOT NULL DEFAULT 1, `tunnel_ping_interval_sec` INTEGER NOT NULL DEFAULT 30, `tunnel_ping_attempts` INTEGER NOT NULL DEFAULT 3, `tunnel_ping_timeout_sec` INTEGER, `show_detailed_ping_stats` INTEGER NOT NULL DEFAULT 0, `is_local_logs_enabled` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isPingMonitoringEnabled",
"columnName": "is_ping_monitoring_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "1"
},
{
"fieldPath": "tunnelPingIntervalSeconds",
"columnName": "tunnel_ping_interval_sec",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "30"
},
{
"fieldPath": "tunnelPingAttempts",
"columnName": "tunnel_ping_attempts",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "3"
},
{
"fieldPath": "tunnelPingTimeoutSeconds",
"columnName": "tunnel_ping_timeout_sec",
"affinity": "INTEGER"
},
{
"fieldPath": "showDetailedPingStats",
"columnName": "show_detailed_ping_stats",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isLocalLogsEnabled",
"columnName": "is_local_logs_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "dns_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `dns_protocol` INTEGER NOT NULL DEFAULT 0, `dns_endpoint` TEXT, `global_tunnel_dns_enabled` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dnsProtocol",
"columnName": "dns_protocol",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "dnsEndpoint",
"columnName": "dns_endpoint",
"affinity": "TEXT"
},
{
"fieldPath": "isGlobalTunnelDnsEnabled",
"columnName": "global_tunnel_dns_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "lockdown_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `bypass_lan` INTEGER NOT NULL DEFAULT 0, `metered` INTEGER NOT NULL DEFAULT 0, `dual_stack` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "bypassLan",
"columnName": "bypass_lan",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "metered",
"columnName": "metered",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "dualStack",
"columnName": "dual_stack",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '98452d8160a1ae66c852ec8cd739e675')"
]
}
}
@@ -64,8 +64,8 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.Appear
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.display.DisplayScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.language.LanguageScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.dns.DnsSettingsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.globals.TunnelGlobalsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.integrations.AndroidIntegrationsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.lockdown.LockdownSettingsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.TunnelMonitoringScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.logs.LogsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.monitoring.ping.PingTargetScreen
@@ -75,9 +75,9 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.support.donate.crypto.Addr
import com.zaneschepke.wireguardautotunnel.ui.screens.support.license.LicenseScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.TunnelsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.config.ConfigScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.settings.TunnelSettingsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.sort.SortScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.splittunnel.SplitTunnelScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.tunneloptions.TunnelOptionsScreen
import com.zaneschepke.wireguardautotunnel.ui.theme.AlertRed
import com.zaneschepke.wireguardautotunnel.ui.theme.OffWhite
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
@@ -331,7 +331,7 @@ class MainActivity : AppCompatActivity() {
}
entry<Route.Tunnels> { TunnelsScreen() }
entry<Route.Sort> { SortScreen() }
entry<Route.TunnelOptions> { key ->
entry<Route.TunnelSettings> { key ->
val viewModel =
hiltViewModel<
TunnelViewModel,
@@ -341,7 +341,7 @@ class MainActivity : AppCompatActivity() {
factory.create(key.id)
}
)
TunnelOptionsScreen(viewModel)
TunnelSettingsScreen(viewModel)
}
entry<Route.SplitTunnel> { key ->
val viewModel =
@@ -388,9 +388,6 @@ class MainActivity : AppCompatActivity() {
AndroidIntegrationsScreen()
}
entry<Route.Dns> { DnsSettingsScreen() }
entry<Route.TunnelGlobals> { key ->
TunnelGlobalsScreen(key.id)
}
entry<Route.ConfigGlobal> { key ->
val viewModel =
hiltViewModel<
@@ -415,6 +412,9 @@ class MainActivity : AppCompatActivity() {
)
SplitTunnelScreen(viewModel)
}
entry<Route.LockdownSettings> {
LockdownSettingsScreen()
}
entry<Route.ProxySettings> { ProxySettingsScreen() }
entry<Route.Appearance> { AppearanceScreen() }
entry<Route.Language> { LanguageScreen() }
@@ -7,18 +7,15 @@ import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationMonitor
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.core.worker.ServiceWorker
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
import dagger.hilt.android.HiltAndroidApp
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
@@ -43,8 +40,6 @@ class WireGuardAutoTunnel : Application(), Configuration.Provider {
@Inject lateinit var notificationMonitor: NotificationMonitor
@Inject lateinit var tunnelManager: TunnelManager
override fun onCreate() {
super.onCreate()
instance = this
@@ -73,12 +68,6 @@ class WireGuardAutoTunnel : Application(), Configuration.Provider {
ServiceWorker.start(this)
}
override fun onTerminate() {
applicationScope.cancel()
tunnelManager.setBackendMode(BackendMode.Inactive)
super.onTerminate()
}
companion object {
private val _uiActive = MutableStateFlow(false)
@@ -35,6 +35,7 @@ class KernelTunnel
constructor(
@ApplicationScope applicationScope: CoroutineScope,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
private val runConfigHelper: RunConfigHelper,
@Kernel private val backend: Backend,
) : BaseTunnel(applicationScope, ioDispatcher) {
@@ -53,7 +54,6 @@ constructor(
return Result.success(Unit)
}
// TODO Add DNS settings
override fun tunnelStateFlow(tunnelConfig: TunnelConfig): Flow<TunnelStatus> = callbackFlow {
validateWireGuardInterfaceName(tunnelConfig.name).onFailure { close(it) }
@@ -69,7 +69,8 @@ constructor(
try {
withTimeout(STARTUP_TIMEOUT_MS) {
updateTunnelStatus(tunnelConfig.id, TunnelStatus.Starting)
backend.setState(runtimeTunnel, WgTunnel.State.UP, tunnelConfig.toWgConfig())
val runConfig = runConfigHelper.buildWgRunConfig(tunnelConfig)
backend.setState(runtimeTunnel, WgTunnel.State.UP, runConfig)
}
} catch (e: TimeoutCancellationException) {
Timber.e("Startup timed out for ${tunnelConfig.name}")
@@ -0,0 +1,104 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.data.model.DnsProtocol
import com.zaneschepke.wireguardautotunnel.domain.events.InvalidConfig
import com.zaneschepke.wireguardautotunnel.domain.model.DnsSettings
import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings
import com.zaneschepke.wireguardautotunnel.domain.model.ProxySettings
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.repository.DnsSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.ProxySettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import java.util.*
import javax.inject.Inject
import kotlinx.coroutines.flow.firstOrNull
import org.amnezia.awg.config.Config
import org.amnezia.awg.config.proxy.HttpProxy
import org.amnezia.awg.config.proxy.Socks5Proxy
class RunConfigHelper
@Inject
constructor(
private val settingsRepository: GeneralSettingRepository,
private val proxySettingsRepository: ProxySettingsRepository,
private val dnsSettingsRepository: DnsSettingsRepository,
private val tunnelsRepository: TunnelRepository,
) {
private data class PrepResult(
val effectiveConfig: TunnelConfig,
val generalSettings: GeneralSettings,
val dnsSettings: DnsSettings,
)
private suspend fun prepare(tunnelConfig: TunnelConfig): PrepResult {
val generalSettings = settingsRepository.getGeneralSettings()
val dnsSettings = dnsSettingsRepository.getDnsSettings()
val effectiveConfig =
if (
generalSettings.isGlobalSplitTunnelEnabled || dnsSettings.isGlobalTunnelDnsEnabled
) {
val globalConfig =
tunnelsRepository.globalTunnelFlow.firstOrNull() ?: throw InvalidConfig()
tunnelConfig.copyWithGlobalValues(
globalConfig,
dnsSettings.isGlobalTunnelDnsEnabled,
generalSettings.isGlobalSplitTunnelEnabled,
)
} else {
tunnelConfig
}
return PrepResult(effectiveConfig, generalSettings, dnsSettings)
}
suspend fun buildAmRunConfig(tunnelConfig: TunnelConfig): Config {
val prep = prepare(tunnelConfig)
val proxies =
if (prep.generalSettings.appMode == AppMode.PROXY) {
val proxySettings = proxySettingsRepository.getProxySettings()
buildList {
if (proxySettings.socks5ProxyEnabled) {
add(
Socks5Proxy(
proxySettings.socks5ProxyBindAddress
?: ProxySettings.DEFAULT_SOCKS_BIND_ADDRESS,
proxySettings.proxyUsername,
proxySettings.proxyPassword,
)
)
}
if (proxySettings.httpProxyEnabled) {
add(
HttpProxy(
proxySettings.httpProxyBindAddress
?: ProxySettings.DEFAULT_HTTP_BIND_ADDRESS,
proxySettings.proxyUsername,
proxySettings.proxyPassword,
)
)
}
}
} else {
emptyList()
}
val amConfig = prep.effectiveConfig.toAmConfig()
return Config.Builder()
.setInterface(amConfig.`interface`)
.addPeers(amConfig.peers)
.addProxies(proxies)
.setDnsSettings(
org.amnezia.awg.config.DnsSettings(
prep.dnsSettings.dnsProtocol == DnsProtocol.DOH,
Optional.ofNullable(prep.dnsSettings.dnsEndpoint),
)
)
.build()
}
suspend fun buildWgRunConfig(tunnelConfig: TunnelConfig): com.wireguard.config.Config {
val prep = prepare(tunnelConfig)
return prep.effectiveConfig.toWgConfig()
}
}
@@ -16,4 +16,6 @@ class RuntimeAwgTunnel(
}
override fun isIpv4ResolutionPreferred() = tunnelConfig.isIpv4Preferred
override fun isMetered() = tunnelConfig.isMetered
}
@@ -1,7 +1,6 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig as Entity
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.di.*
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
@@ -14,6 +13,7 @@ import com.zaneschepke.wireguardautotunnel.domain.model.GeneralSettings
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.LockdownSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.state.LogHealthState
import com.zaneschepke.wireguardautotunnel.domain.state.PingState
@@ -40,6 +40,7 @@ constructor(
private val serviceManager: ServiceManager,
private val settingsRepository: GeneralSettingRepository,
private val autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
private val lockdownSettingsRepository: LockdownSettingsRepository,
private val tunnelsRepository: TunnelRepository,
private val tunnelMonitor: TunnelMonitor,
@ApplicationScope private val applicationScope: CoroutineScope,
@@ -70,12 +71,6 @@ constructor(
val condition: (SideEffectState) -> Boolean,
)
private suspend fun getSettings(): GeneralSettings =
settingsRepository.flow.filterNotNull().first { it != GeneralSettings() }
private suspend fun getTunnels(): List<TunnelConfig> =
tunnelsRepository.flow.first { it.isNotEmpty() }
private val tunnelProviderFlow: StateFlow<TunnelProvider> = run {
val currentBackend = AtomicReference(userspaceTunnel)
val currentSettings = AtomicReference(GeneralSettings())
@@ -85,10 +80,7 @@ constructor(
.filterNotNull()
// ignore default state
.filterNot { it == GeneralSettings() }
.distinctUntilChanged { old, new ->
old.appMode == new.appMode &&
old.isLanOnKillSwitchEnabled == new.isLanOnKillSwitchEnabled
}
.distinctUntilChangedBy { it.appMode }
.map { settings ->
Timber.d("App mode changes with ${settings.appMode}")
val backend =
@@ -109,7 +101,7 @@ constructor(
handleModeChangeCleanup(previousBackend, previousSettings.appMode)
}
if (settings.appMode == AppMode.LOCK_DOWN) {
handleLockDownModeInit(settings.isLanOnKillSwitchEnabled)
handleLockDownModeInit()
}
}
.map { (_, backend) -> backend }
@@ -236,17 +228,7 @@ constructor(
activeTunnels.first { it.isEmpty() }
} ?: run { activeTunnels.value.keys.forEach { id -> provider.forceStopTunnel(id) } }
}
val runConfig =
tunnelConfig.run {
if (getSettings().isTunnelGlobalsEnabled) {
val globalTunnel =
getTunnels().firstOrNull { it.name == Entity.GLOBAL_CONFIG_NAME }
?: return@run this
return@run copyWithGlobalValues(globalTunnel)
}
this
}
tunnelProviderFlow.value.startTunnel(runConfig)
tunnelProviderFlow.value.startTunnel(tunnelConfig)
}
override suspend fun stopTunnel(tunnelId: Int) {
@@ -303,11 +285,21 @@ constructor(
serviceManager.updateTunnelTile()
}
private fun handleLockDownModeInit(withLanBypass: Boolean) {
val allowedIps = if (withLanBypass) TunnelConfig.IPV4_PUBLIC_NETWORKS else emptySet()
// TODO this can crash if we haven't started foreground service yet, especially for
// workerManager
private suspend fun handleLockDownModeInit() {
val lockdownSettings = lockdownSettingsRepository.getLockdownSettings()
val allowedIps =
if (lockdownSettings.bypassLan) TunnelConfig.IPV4_PUBLIC_NETWORKS else emptySet()
try {
if (serviceManager.hasVpnPermission()) {
proxyUserspaceTunnel.setBackendMode(BackendMode.KillSwitch(allowedIps))
proxyUserspaceTunnel.setBackendMode(
BackendMode.KillSwitch(
allowedIps,
lockdownSettings.metered,
lockdownSettings.dualStack,
)
)
} else {
throw NotAuthorized()
}
@@ -350,8 +342,7 @@ constructor(
AppMode.VPN,
AppMode.PROXY,
AppMode.LOCK_DOWN -> {
if (mode == AppMode.LOCK_DOWN)
handleLockDownModeInit(settings.isLanOnKillSwitchEnabled)
if (mode == AppMode.LOCK_DOWN) handleLockDownModeInit()
tunnels.firstOrNull { it.isActive }?.let { startTunnel(it) }
}
AppMode.KERNEL ->
@@ -378,8 +369,7 @@ constructor(
tunnelsRepository.resetActiveTunnels()
if (isVpnAuthorized(settings.appMode)) {
when (val mode = settings.appMode) {
AppMode.LOCK_DOWN ->
handleLockDownModeInit(settings.isLanOnKillSwitchEnabled)
AppMode.LOCK_DOWN -> handleLockDownModeInit()
AppMode.KERNEL,
AppMode.VPN,
AppMode.PROXY -> Unit
@@ -1,5 +1,6 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import android.os.PowerManager
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.data.model.AppMode
@@ -31,6 +32,7 @@ constructor(
private val networkMonitor: NetworkMonitor,
private val networkUtils: NetworkUtils,
private val logReader: LogReader,
private val powerManager: PowerManager,
) {
@OptIn(FlowPreview::class)
@@ -74,7 +76,7 @@ constructor(
else -> null
}
}
.distinctUntilChangedBy { it.isHealthy } // Only emit when health changes
.distinctUntilChangedBy { it.isHealthy }
.collect { logHealthState ->
Timber.d("Tunnel log health updated for ${tunnelConfig.name}: $logHealthState")
updateTunnelStatus(tunnelConfig.id, null, null, null, logHealthState)
@@ -199,6 +201,7 @@ constructor(
}
val attemptTime = System.currentTimeMillis()
val timeout = settings.tunnelPingTimeoutSeconds?.toMillis() ?: 5000L
runCatching {
withTimeout(
settings.tunnelPingTimeoutSeconds?.toMillis() ?: 5000L
@@ -270,20 +273,28 @@ constructor(
while (isActive) {
ensureActive()
if (isNetworkConnected.value) {
performPing()
} else {
pingStatsFlow.update { current ->
current.mapValues { entry ->
entry.value.copy(
isReachable = false,
failureReason = FailureReason.NoConnectivity,
lastPingAttemptMillis = System.currentTimeMillis(),
)
if (!powerManager.isDeviceIdleMode) {
if (isNetworkConnected.value) {
performPing()
} else {
pingStatsFlow.update { current ->
current.mapValues { entry ->
entry.value.copy(
isReachable = false,
failureReason = FailureReason.NoConnectivity,
lastPingAttemptMillis = System.currentTimeMillis(),
)
}
}
ensureActive()
updateTunnelStatus(
tunnelConfig.id,
null,
null,
pingStatsFlow.value,
null,
)
}
ensureActive()
updateTunnelStatus(tunnelConfig.id, null, null, pingStatsFlow.value, null)
}
delay(settings.tunnelPingIntervalSeconds.toMillis())
}
@@ -300,9 +311,11 @@ constructor(
) = coroutineScope {
while (isActive) {
ensureActive()
val stats = getStatistics(tunnelId)
ensureActive()
updateTunnelStatus(tunnelId, null, stats, null, null)
if (!powerManager.isDeviceIdleMode) {
val stats = getStatistics(tunnelId)
ensureActive()
updateTunnelStatus(tunnelId, null, stats, null, null)
}
delay(STATS_DELAY)
}
}
@@ -1,19 +1,11 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.data.model.DnsProtocol
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendMode
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.events.DnsFailure
import com.zaneschepke.wireguardautotunnel.domain.events.InvalidConfig
import com.zaneschepke.wireguardautotunnel.domain.events.ServiceNotRunning
import com.zaneschepke.wireguardautotunnel.domain.events.UnknownError
import com.zaneschepke.wireguardautotunnel.domain.events.VpnUnauthorized
import com.zaneschepke.wireguardautotunnel.domain.model.ProxySettings
import com.zaneschepke.wireguardautotunnel.domain.events.*
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.repository.DnsSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.ProxySettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.state.AmneziaStatistics
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.util.extensions.asAmBackendMode
@@ -21,7 +13,6 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.asBackendMode
import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendCoreException
import java.io.IOException
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
import kotlinx.coroutines.*
@@ -33,13 +24,7 @@ import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.update
import org.amnezia.awg.backend.Backend
import org.amnezia.awg.backend.BackendException
import org.amnezia.awg.backend.ProxyGoBackend
import org.amnezia.awg.backend.Tunnel as AwgTunnel
import org.amnezia.awg.config.Config
import org.amnezia.awg.config.DnsSettings
import org.amnezia.awg.config.proxy.HttpProxy
import org.amnezia.awg.config.proxy.Proxy
import org.amnezia.awg.config.proxy.Socks5Proxy
import timber.log.Timber
class UserspaceTunnel
@@ -47,9 +32,8 @@ class UserspaceTunnel
constructor(
@ApplicationScope applicationScope: CoroutineScope,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
private val proxySettingsRepository: ProxySettingsRepository,
private val dnsSettingsRepository: DnsSettingsRepository,
private val backend: Backend,
private val runConfigHelper: RunConfigHelper,
) : BaseTunnel(applicationScope, ioDispatcher) {
private val runtimeTunnels = ConcurrentHashMap<Int, AwgTunnel>()
@@ -67,63 +51,17 @@ constructor(
try {
withTimeout(STARTUP_TIMEOUT_MS) {
updateTunnelStatus(tunnelConfig.id, TunnelStatus.Starting)
val proxies: List<Proxy> =
when (backend) {
is ProxyGoBackend -> {
val proxySettings = proxySettingsRepository.getProxySettings()
Timber.d("Adding proxy configs")
buildList {
if (proxySettings.socks5ProxyEnabled) {
add(
Socks5Proxy(
proxySettings.socks5ProxyBindAddress
?: ProxySettings.DEFAULT_SOCKS_BIND_ADDRESS,
proxySettings.proxyUsername,
proxySettings.proxyPassword,
)
)
}
if (proxySettings.httpProxyEnabled) {
add(
HttpProxy(
proxySettings.httpProxyBindAddress
?: ProxySettings.DEFAULT_HTTP_BIND_ADDRESS,
proxySettings.proxyUsername,
proxySettings.proxyPassword,
)
)
}
}
}
else -> emptyList()
}
val setting = dnsSettingsRepository.getDnsSettings()
val config = tunnelConfig.toAmConfig()
val updatedConfig =
Config.Builder()
.apply {
setInterface(config.`interface`)
addPeers(config.peers)
addProxies(proxies)
setDnsSettings(
DnsSettings(
setting.dnsProtocol == DnsProtocol.DOH,
Optional.ofNullable(setting.dnsEndpoint),
)
)
}
.build()
backend.setState(runtimeTunnel, AwgTunnel.State.UP, updatedConfig)
val runConfig = runConfigHelper.buildAmRunConfig(tunnelConfig)
backend.setState(runtimeTunnel, AwgTunnel.State.UP, runConfig)
}
} catch (e: TimeoutCancellationException) {
} catch (_: TimeoutCancellationException) {
Timber.e("Startup timed out for ${tunnelConfig.name} (likely DNS hang)")
errors.emit(tunnelConfig.name to DnsFailure())
forceStopTunnel(tunnelConfig.id)
close()
} catch (e: BackendException) {
close(e.toBackendCoreException())
} catch (e: IllegalArgumentException) {
} catch (_: IllegalArgumentException) {
close(InvalidConfig())
} catch (e: Exception) {
Timber.e(e, "Error while setting tunnel state")
@@ -15,8 +15,9 @@ import com.zaneschepke.wireguardautotunnel.data.entity.*
AutoTunnelSettings::class,
MonitoringSettings::class,
DnsSettings::class,
LockdownSettings::class,
],
version = 25,
version = 27,
autoMigrations =
[
AutoMigration(from = 1, to = 2),
@@ -42,6 +43,7 @@ import com.zaneschepke.wireguardautotunnel.data.entity.*
AutoMigration(from = 21, to = 22),
AutoMigration(from = 22, to = 23),
AutoMigration(from = 24, to = 25),
AutoMigration(from = 26, to = 27, spec = GlobalsMigration::class),
],
exportSchema = true,
)
@@ -57,6 +59,8 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun monitoringSettingsDao(): MonitoringSettingsDao
abstract fun lockdownSettingsDao(): LockdownSettingsDao
abstract fun dnsSettingsDao(): DnsSettingsDao
}
@@ -112,3 +116,12 @@ class FixProxySettingsMigration : AutoMigrationSpec {
}
}
}
@RenameColumn.Entries(
RenameColumn(
tableName = "general_settings",
fromColumnName = "is_tunnel_globals_enabled",
toColumnName = "global_split_tunnel_enabled",
)
)
class GlobalsMigration : AutoMigrationSpec
@@ -0,0 +1,18 @@
package com.zaneschepke.wireguardautotunnel.data.dao
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Upsert
import com.zaneschepke.wireguardautotunnel.data.entity.LockdownSettings
import kotlinx.coroutines.flow.Flow
@Dao
interface LockdownSettingsDao {
@Query("SELECT * FROM lockdown_settings LIMIT 1")
suspend fun getLockdownSettings(): LockdownSettings?
@Upsert suspend fun upsert(lockdownSettings: LockdownSettings)
@Query("SELECT * FROM lockdown_settings LIMIT 1")
fun getLockdownSettingsFlow(): Flow<LockdownSettings?>
}
@@ -11,4 +11,6 @@ data class DnsSettings(
@ColumnInfo(name = "dns_protocol", defaultValue = "0")
val dnsProtocol: DnsProtocol = DnsProtocol.fromValue(0),
@ColumnInfo(name = "dns_endpoint") val dnsEndpoint: String? = null,
@ColumnInfo(name = "global_tunnel_dns_enabled", defaultValue = "0")
val isGlobalTunnelDnsEnabled: Boolean = false,
)
@@ -14,8 +14,8 @@ data class GeneralSettings(
val isRestoreOnBootEnabled: Boolean = false,
@ColumnInfo(name = "is_multi_tunnel_enabled", defaultValue = "0")
val isMultiTunnelEnabled: Boolean = false,
@ColumnInfo(name = "is_tunnel_globals_enabled", defaultValue = "0")
val isTunnelGlobalsEnabled: Boolean = false,
@ColumnInfo(name = "global_split_tunnel_enabled", defaultValue = "0")
val isGlobalSplitTunnelEnabled: Boolean = false,
@ColumnInfo(name = "app_mode", defaultValue = "0") val appMode: AppMode = AppMode.fromValue(0),
@ColumnInfo(name = "theme", defaultValue = "AUTOMATIC") val theme: String = "AUTOMATIC",
@ColumnInfo(name = "locale") val locale: String? = null,
@@ -26,8 +26,6 @@ data class GeneralSettings(
val isPinLockEnabled: Boolean = false,
@ColumnInfo(name = "is_always_on_vpn_enabled", defaultValue = "0")
val isAlwaysOnVpnEnabled: Boolean = false,
@ColumnInfo(name = "is_lan_on_kill_switch_enabled", defaultValue = "0")
val isLanOnKillSwitchEnabled: Boolean = false,
@ColumnInfo(name = "custom_split_packages", defaultValue = "{}")
val customSplitPackages: Map<String, String> = emptyMap(),
)
@@ -0,0 +1,13 @@
package com.zaneschepke.wireguardautotunnel.data.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "lockdown_settings")
data class LockdownSettings(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
@ColumnInfo(name = "bypass_lan", defaultValue = "0") val bypassLan: Boolean = false,
@ColumnInfo(name = "metered", defaultValue = "0") val metered: Boolean = false,
@ColumnInfo(name = "dual_stack", defaultValue = "0") val dualStack: Boolean = false,
)
@@ -28,8 +28,8 @@ data class TunnelConfig(
@ColumnInfo(name = "position", defaultValue = "0") val position: Int = 0,
@ColumnInfo(name = "auto_tunnel_apps", defaultValue = "[]")
val autoTunnelApps: Set<String> = emptySet(),
@ColumnInfo(name = "is_metered", defaultValue = "true") val isMetered: Boolean = true,
) {
companion object {
const val GLOBAL_CONFIG_NAME = "4675ab06-903a-438b-8485-6ea4187a9512"
}
@@ -4,7 +4,17 @@ import com.zaneschepke.wireguardautotunnel.data.entity.DnsSettings as Entity
import com.zaneschepke.wireguardautotunnel.domain.model.DnsSettings as Domain
fun Entity.toDomain(): Domain =
Domain(id = id, dnsProtocol = dnsProtocol, dnsEndpoint = dnsEndpoint)
Domain(
id = id,
dnsProtocol = dnsProtocol,
dnsEndpoint = dnsEndpoint,
isGlobalTunnelDnsEnabled = isGlobalTunnelDnsEnabled,
)
fun Domain.toEntity(): Entity =
Entity(id = id, dnsProtocol = dnsProtocol, dnsEndpoint = dnsEndpoint)
Entity(
id = id,
dnsProtocol = dnsProtocol,
dnsEndpoint = dnsEndpoint,
isGlobalTunnelDnsEnabled = isGlobalTunnelDnsEnabled,
)
@@ -0,0 +1,10 @@
package com.zaneschepke.wireguardautotunnel.data.mapper
import com.zaneschepke.wireguardautotunnel.data.entity.LockdownSettings as Entity
import com.zaneschepke.wireguardautotunnel.domain.model.LockdownSettings as Domain
fun Entity.toDomain(): Domain =
Domain(id = id, bypassLan = bypassLan, metered = metered, dualStack = dualStack)
fun Domain.toEntity(): Entity =
Entity(id = id, bypassLan = bypassLan, metered = metered, dualStack = dualStack)
@@ -10,7 +10,7 @@ fun Entity.toDomain(): Domain =
isShortcutsEnabled = isShortcutsEnabled,
isRestoreOnBootEnabled = isRestoreOnBootEnabled,
isMultiTunnelEnabled = isMultiTunnelEnabled,
isTunnelGlobalsEnabled = isTunnelGlobalsEnabled,
isGlobalSplitTunnelEnabled = isGlobalSplitTunnelEnabled,
appMode = appMode,
theme = Theme.valueOf(theme.uppercase()),
locale = locale,
@@ -18,8 +18,6 @@ fun Entity.toDomain(): Domain =
isRemoteControlEnabled = isRemoteControlEnabled,
isPinLockEnabled = isPinLockEnabled,
isAlwaysOnVpnEnabled = isAlwaysOnVpnEnabled,
isLanOnKillSwitchEnabled = isLanOnKillSwitchEnabled,
customSplitPackages = customSplitPackages,
)
fun Domain.toEntity(): Entity =
@@ -28,7 +26,7 @@ fun Domain.toEntity(): Entity =
isShortcutsEnabled = isShortcutsEnabled,
isRestoreOnBootEnabled = isRestoreOnBootEnabled,
isMultiTunnelEnabled = isMultiTunnelEnabled,
isTunnelGlobalsEnabled = isTunnelGlobalsEnabled,
isGlobalSplitTunnelEnabled = isGlobalSplitTunnelEnabled,
appMode = appMode,
theme = theme.name,
locale = locale,
@@ -36,6 +34,4 @@ fun Domain.toEntity(): Entity =
isRemoteControlEnabled = isRemoteControlEnabled,
isPinLockEnabled = isPinLockEnabled,
isAlwaysOnVpnEnabled = isAlwaysOnVpnEnabled,
isLanOnKillSwitchEnabled = isLanOnKillSwitchEnabled,
customSplitPackages = customSplitPackages,
)
@@ -19,6 +19,7 @@ fun Entity.toDomain(): Domain =
isIpv4Preferred = isIpv4Preferred,
position = position,
autoTunnelApps = autoTunnelApps,
isMetered = isMetered,
)
fun Domain.toEntity(): Entity =
@@ -37,4 +38,5 @@ fun Domain.toEntity(): Entity =
isIpv4Preferred = isIpv4Preferred,
position = position,
autoTunnelApps = autoTunnelApps,
isMetered = isMetered,
)
@@ -316,3 +316,98 @@ val MIGRATION_23_24 =
}
}
}
val MIGRATION_25_26 =
object : Migration(25, 26) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL(
"""
CREATE TABLE IF NOT EXISTS `lockdown_settings` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
`bypass_lan` INTEGER NOT NULL DEFAULT 0,
`metered` INTEGER NOT NULL DEFAULT 0,
`dual_stack` INTEGER NOT NULL DEFAULT 0
)
"""
.trimIndent()
)
val cursor =
db.query("SELECT `is_lan_on_kill_switch_enabled` FROM `general_settings` LIMIT 1")
var bypassLan = 0
if (cursor.moveToFirst()) {
bypassLan = if (cursor.getInt(0) != 0) 1 else 0
}
cursor.close()
db.execSQL(
"""
INSERT INTO `lockdown_settings` (`bypass_lan`, `metered`, `dual_stack`)
VALUES (?, 0, 0)
"""
.trimIndent(),
arrayOf(bypassLan),
)
db.execSQL(
"""
CREATE TABLE IF NOT EXISTS `general_settings_new` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
`is_shortcuts_enabled` INTEGER NOT NULL DEFAULT 0,
`is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT 0,
`is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT 0,
`is_tunnel_globals_enabled` INTEGER NOT NULL DEFAULT 0,
`app_mode` INTEGER NOT NULL DEFAULT 0,
`theme` TEXT NOT NULL DEFAULT 'AUTOMATIC',
`locale` TEXT,
`remote_key` TEXT,
`is_remote_control_enabled` INTEGER NOT NULL DEFAULT 0,
`is_pin_lock_enabled` INTEGER NOT NULL DEFAULT 0,
`is_always_on_vpn_enabled` INTEGER NOT NULL DEFAULT 0,
`custom_split_packages` TEXT NOT NULL DEFAULT '{}'
)
"""
.trimIndent()
)
db.execSQL(
"""
INSERT INTO `general_settings_new` (
`id`,
`is_shortcuts_enabled`,
`is_restore_on_boot_enabled`,
`is_multi_tunnel_enabled`,
`is_tunnel_globals_enabled`,
`app_mode`,
`theme`,
`locale`,
`remote_key`,
`is_remote_control_enabled`,
`is_pin_lock_enabled`,
`is_always_on_vpn_enabled`,
`custom_split_packages`
)
SELECT
`id`,
`is_shortcuts_enabled`,
`is_restore_on_boot_enabled`,
`is_multi_tunnel_enabled`,
`is_tunnel_globals_enabled`,
`app_mode`,
`theme`,
`locale`,
`remote_key`,
`is_remote_control_enabled`,
`is_pin_lock_enabled`,
`is_always_on_vpn_enabled`,
`custom_split_packages`
FROM `general_settings`
"""
.trimIndent()
)
db.execSQL("DROP TABLE `general_settings`")
db.execSQL("ALTER TABLE `general_settings_new` RENAME TO `general_settings`")
}
}
@@ -0,0 +1,34 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.dao.LockdownSettingsDao
import com.zaneschepke.wireguardautotunnel.data.entity.LockdownSettings as Entity
import com.zaneschepke.wireguardautotunnel.data.mapper.toDomain
import com.zaneschepke.wireguardautotunnel.data.mapper.toEntity
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.model.LockdownSettings as Domain
import com.zaneschepke.wireguardautotunnel.domain.repository.LockdownSettingsRepository
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
class RoomLockdownSettingsRepository(
private val lockdownSettingsDao: LockdownSettingsDao,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : LockdownSettingsRepository {
override suspend fun upsert(lockdownSettings: Domain) {
withContext(ioDispatcher) { lockdownSettingsDao.upsert(lockdownSettings.toEntity()) }
}
override val flow =
lockdownSettingsDao
.getLockdownSettingsFlow()
.map { (it ?: Entity()).toDomain() }
.flowOn(ioDispatcher)
override suspend fun getLockdownSettings(): Domain {
return withContext(ioDispatcher) {
(lockdownSettingsDao.getLockdownSettings() ?: Entity()).toDomain()
}
}
}
@@ -23,7 +23,6 @@ class RoomProxySettingsRepository(
override val flow =
proxySettingsDao
.getProxySettingsFlow()
.flowOn(ioDispatcher)
.map { (it ?: Entity()).toDomain() }
.flowOn(ioDispatcher)
@@ -8,6 +8,7 @@ import com.zaneschepke.wireguardautotunnel.data.DataStoreManager
import com.zaneschepke.wireguardautotunnel.data.DatabaseCallback
import com.zaneschepke.wireguardautotunnel.data.dao.*
import com.zaneschepke.wireguardautotunnel.data.migrations.MIGRATION_23_24
import com.zaneschepke.wireguardautotunnel.data.migrations.MIGRATION_25_26
import com.zaneschepke.wireguardautotunnel.data.network.GitHubApi
import com.zaneschepke.wireguardautotunnel.data.network.KtorClient
import com.zaneschepke.wireguardautotunnel.data.network.KtorGitHubApi
@@ -56,6 +57,7 @@ class RepositoryModule {
context.getString(R.string.db_name),
)
.addMigrations(MIGRATION_23_24(dataStoreManager.dataStore))
.addMigrations(MIGRATION_25_26)
.fallbackToDestructiveMigration(true)
.addCallback(callback)
.build()
@@ -67,6 +69,12 @@ class RepositoryModule {
return appDatabase.generalSettingsDao()
}
@Singleton
@Provides
fun provideLockdownDoa(appDatabase: AppDatabase): LockdownSettingsDao {
return appDatabase.lockdownSettingsDao()
}
@Singleton
@Provides
fun provideDnsSettingsDao(appDatabase: AppDatabase): DnsSettingsDao {
@@ -106,6 +114,15 @@ class RepositoryModule {
return RoomTunnelRepository(tunnelConfigDao, ioDispatcher)
}
@Singleton
@Provides
fun provideLockdownSettingsRepository(
lockdownSettingsDao: LockdownSettingsDao,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): LockdownSettingsRepository {
return RoomLockdownSettingsRepository(lockdownSettingsDao, ioDispatcher)
}
@Singleton
@Provides
fun provideGeneralSettingsRepository(
@@ -1,6 +1,7 @@
package com.zaneschepke.wireguardautotunnel.di
import android.content.Context
import android.os.PowerManager
import com.wireguard.android.backend.WgQuickBackend
import com.wireguard.android.util.RootShell
import com.wireguard.android.util.ToolsInstaller
@@ -85,8 +86,9 @@ class TunnelModule {
@ApplicationScope applicationScope: CoroutineScope,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
backend: com.wireguard.android.backend.Backend,
runConfigHelper: RunConfigHelper,
): TunnelProvider {
return KernelTunnel(applicationScope, ioDispatcher, backend)
return KernelTunnel(applicationScope, ioDispatcher, runConfigHelper, backend)
}
@Provides
@@ -94,18 +96,11 @@ class TunnelModule {
@Userspace
fun provideUserspaceProvider(
@ApplicationScope applicationScope: CoroutineScope,
proxySettingsRepository: ProxySettingsRepository,
dnsSettingsRepository: DnsSettingsRepository,
runConfigHelper: RunConfigHelper,
@Userspace backend: Backend,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): TunnelProvider {
return UserspaceTunnel(
applicationScope,
ioDispatcher,
proxySettingsRepository,
dnsSettingsRepository,
backend,
)
return UserspaceTunnel(applicationScope, ioDispatcher, backend, runConfigHelper)
}
@Provides
@@ -113,18 +108,11 @@ class TunnelModule {
@ProxyUserspace
fun provideProxyUserspaceProvider(
@ApplicationScope applicationScope: CoroutineScope,
dnsSettingsRepository: DnsSettingsRepository,
proxySettingsRepository: ProxySettingsRepository,
runConfigHelper: RunConfigHelper,
@ProxyUserspace backend: Backend,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): TunnelProvider {
return UserspaceTunnel(
applicationScope,
ioDispatcher,
proxySettingsRepository,
dnsSettingsRepository,
backend,
)
return UserspaceTunnel(applicationScope, ioDispatcher, backend, runConfigHelper)
}
@Provides
@@ -135,6 +123,7 @@ class TunnelModule {
@ProxyUserspace proxyTunnel: TunnelProvider,
serviceManager: ServiceManager,
tunnelRepository: TunnelRepository,
lockdownSettingsRepository: LockdownSettingsRepository,
settingsRepository: GeneralSettingRepository,
autoTunnelSettingsRepository: AutoTunnelSettingsRepository,
tunnelMonitor: TunnelMonitor,
@@ -148,6 +137,7 @@ class TunnelModule {
serviceManager,
settingsRepository,
autoTunnelSettingsRepository,
lockdownSettingsRepository,
tunnelRepository,
tunnelMonitor,
applicationScope,
@@ -155,6 +145,22 @@ class TunnelModule {
)
}
@Provides
@Singleton
fun provideTunnelConfigHelper(
settingsRepository: GeneralSettingRepository,
proxySettingsRepository: ProxySettingsRepository,
dnsSettingsRepository: DnsSettingsRepository,
tunnelRepository: TunnelRepository,
): RunConfigHelper {
return RunConfigHelper(
settingsRepository,
proxySettingsRepository,
dnsSettingsRepository,
tunnelRepository,
)
}
@Provides
@Singleton
fun provideNetworkMonitor(
@@ -200,6 +206,7 @@ class TunnelModule {
@Singleton
@Provides
fun provideTunnelMonitor(
@ApplicationContext context: Context,
networkMonitor: NetworkMonitor,
networkUtils: NetworkUtils,
logReader: LogReader,
@@ -214,6 +221,7 @@ class TunnelModule {
networkMonitor,
networkUtils,
logReader,
context.getSystemService(Context.POWER_SERVICE) as PowerManager,
)
}
}
@@ -3,5 +3,9 @@ package com.zaneschepke.wireguardautotunnel.domain.enums
sealed class BackendMode {
data object Inactive : BackendMode()
data class KillSwitch(val allowedIps: Set<String>) : BackendMode()
data class KillSwitch(
val allowedIps: Set<String>,
val isMetered: Boolean,
val dualStack: Boolean,
) : BackendMode()
}
@@ -6,4 +6,5 @@ data class DnsSettings(
val id: Int = 0,
val dnsProtocol: DnsProtocol = DnsProtocol.fromValue(0),
val dnsEndpoint: String? = null,
val isGlobalTunnelDnsEnabled: Boolean = false,
)
@@ -8,7 +8,7 @@ data class GeneralSettings(
val isShortcutsEnabled: Boolean = false,
val isRestoreOnBootEnabled: Boolean = false,
val isMultiTunnelEnabled: Boolean = false,
val isTunnelGlobalsEnabled: Boolean = false,
val isGlobalSplitTunnelEnabled: Boolean = false,
val appMode: AppMode = AppMode.fromValue(0),
val theme: Theme = Theme.AUTOMATIC,
val locale: String? = null,
@@ -16,6 +16,5 @@ data class GeneralSettings(
val isRemoteControlEnabled: Boolean = false,
val isPinLockEnabled: Boolean = false,
val isAlwaysOnVpnEnabled: Boolean = false,
val isLanOnKillSwitchEnabled: Boolean = false,
val customSplitPackages: Map<String, String> = emptyMap(),
val isKillSwitchMetered: Boolean = true,
)
@@ -0,0 +1,8 @@
package com.zaneschepke.wireguardautotunnel.domain.model
data class LockdownSettings(
val id: Long = 0L,
val bypassLan: Boolean = false,
val metered: Boolean = false,
val dualStack: Boolean = false,
)
@@ -27,6 +27,7 @@ data class TunnelConfig(
val isIpv4Preferred: Boolean = true,
val position: Int = 0,
val autoTunnelApps: Set<String> = setOf(),
val isMetered: Boolean = true,
) {
override fun equals(other: Any?): Boolean {
@@ -42,7 +43,8 @@ data class TunnelConfig(
pingTarget == other.pingTarget &&
restartOnPingFailure == other.restartOnPingFailure &&
tunnelNetworks == other.tunnelNetworks &&
isIpv4Preferred == other.isIpv4Preferred
isIpv4Preferred == other.isIpv4Preferred &&
isMetered == other.isMetered
}
override fun hashCode(): Int {
@@ -65,7 +67,11 @@ data class TunnelConfig(
return configFromWgQuick(wgQuick)
}
fun copyWithGlobalValues(globalTunnel: TunnelConfig): TunnelConfig {
fun copyWithGlobalValues(
globalTunnel: TunnelConfig,
includeDns: Boolean,
includeSpitTunneling: Boolean,
): TunnelConfig {
val existingConfig = toAmConfig()
val globalConfig = globalTunnel.toAmConfig()
@@ -114,62 +120,14 @@ data class TunnelConfig(
setPreDown(existingConfig.`interface`.preDown)
setPostDown(existingConfig.`interface`.postDown)
globalConfig.`interface`.mtu.ifPresent { setMtu(it) }
if (globalConfig.`interface`.dnsServers.isNotEmpty()) {
if (includeDns) {
setDnsServers(globalConfig.`interface`.dnsServers)
}
if (globalConfig.`interface`.dnsSearchDomains.isNotEmpty()) {
setDnsSearchDomains(globalConfig.`interface`.dnsSearchDomains)
}
if (globalConfig.`interface`.excludedApplications.isNotEmpty()) {
if (includeSpitTunneling) {
setExcludedApplications(globalConfig.`interface`.excludedApplications)
}
if (!globalConfig.`interface`.includedApplications.isEmpty()) {
setIncludedApplications(globalConfig.`interface`.includedApplications)
}
if (globalConfig.`interface`.preUp.isNotEmpty()) {
setPreUp(globalConfig.`interface`.preUp)
}
if (globalConfig.`interface`.postUp.isNotEmpty()) {
setPostUp(globalConfig.`interface`.postUp)
}
if (globalConfig.`interface`.preDown.isNotEmpty()) {
setPreDown(globalConfig.`interface`.preDown)
}
if (globalConfig.`interface`.postDown.isNotEmpty()) {
setPostDown(globalConfig.`interface`.postDown)
}
globalConfig.`interface`.junkPacketCount.ifPresent { setJunkPacketCount(it) }
globalConfig.`interface`.junkPacketMinSize.ifPresent { setJunkPacketMinSize(it) }
globalConfig.`interface`.junkPacketMaxSize.ifPresent { setJunkPacketMaxSize(it) }
globalConfig.`interface`.initPacketJunkSize.ifPresent { setInitPacketJunkSize(it) }
globalConfig.`interface`.responsePacketJunkSize.ifPresent {
setResponsePacketJunkSize(it)
}
globalConfig.`interface`.initPacketMagicHeader.ifPresent {
setInitPacketMagicHeader(it)
}
globalConfig.`interface`.responsePacketMagicHeader.ifPresent {
setResponsePacketMagicHeader(it)
}
globalConfig.`interface`.underloadPacketMagicHeader.ifPresent {
setUnderloadPacketMagicHeader(it)
}
globalConfig.`interface`.transportPacketMagicHeader.ifPresent {
setTransportPacketMagicHeader(it)
}
globalConfig.`interface`.i1.ifPresent { setI1(it) }
globalConfig.`interface`.i2.ifPresent { setI2(it) }
globalConfig.`interface`.i3.ifPresent { setI3(it) }
globalConfig.`interface`.i4.ifPresent { setI4(it) }
globalConfig.`interface`.i5.ifPresent { setI5(it) }
globalConfig.`interface`.j1.ifPresent { setJ1(it) }
globalConfig.`interface`.j2.ifPresent { setJ2(it) }
globalConfig.`interface`.j3.ifPresent { setJ3(it) }
globalConfig.`interface`.itime.ifPresent { setItime(it) }
}
val newInterface = newInterfaceBuilder.build()
@@ -0,0 +1,12 @@
package com.zaneschepke.wireguardautotunnel.domain.repository
import com.zaneschepke.wireguardautotunnel.domain.model.LockdownSettings
import kotlinx.coroutines.flow.Flow
interface LockdownSettingsRepository {
suspend fun upsert(lockdownSettings: LockdownSettings)
val flow: Flow<LockdownSettings>
suspend fun getLockdownSettings(): LockdownSettings
}
@@ -0,0 +1,46 @@
package com.zaneschepke.wireguardautotunnel.ui.common.button
import android.R.attr.onClick
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ExpandMore
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.VerticalDivider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
@Composable
fun SheetButtonWithDivider(
showDivider: Boolean = true,
modifier: Modifier = Modifier,
onClick: () -> Unit,
) {
Row(
modifier = Modifier.height(IntrinsicSize.Min),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
if (showDivider) {
VerticalDivider(
modifier = Modifier.fillMaxHeight().padding(horizontal = 8.dp, vertical = 8.dp),
color = MaterialTheme.colorScheme.outline,
)
}
Box(modifier = Modifier.pointerInput(Unit) { detectTapGestures {} }) {
IconButton(onClick = onClick, modifier) {
Icon(
Icons.Outlined.ExpandMore,
contentDescription = stringResource(R.string.select),
)
}
}
}
}
@@ -34,7 +34,7 @@ sealed class Route : NavKey {
@Keep @Serializable data object Tunnels : Route()
@Keep @Serializable data class TunnelOptions(val id: Int) : Route()
@Keep @Serializable data class TunnelSettings(val id: Int) : Route()
@Keep @Serializable data class Config(val id: Int?) : Route()
@@ -42,8 +42,6 @@ sealed class Route : NavKey {
@Keep @Serializable data class ConfigGlobal(val id: Int?) : Route()
@Keep @Serializable data class TunnelGlobals(val id: Int) : Route()
@Keep @Serializable data class SplitTunnelGlobal(val id: Int) : Route()
@Keep @Serializable data object Sort : Route()
@@ -58,6 +56,8 @@ sealed class Route : NavKey {
@Keep @Serializable data object ProxySettings : Route()
@Keep @Serializable data object LockdownSettings : Route()
@Keep @Serializable data object AutoTunnel : Route()
@Keep @Serializable data object AdvancedAutoTunnel : Route()
@@ -107,7 +107,7 @@ enum class Tab(
when (route) {
is Route.Tunnels,
Route.Sort,
is Route.TunnelOptions,
is Route.TunnelSettings,
is Route.Config,
is Route.Lock,
is Route.SplitTunnel -> TUNNELS
@@ -121,14 +121,14 @@ enum class Tab(
Route.TunnelMonitoring,
Route.AndroidIntegrations,
Route.Dns,
is Route.TunnelGlobals,
is Route.ConfigGlobal,
is Route.SplitTunnelGlobal,
Route.ProxySettings,
Route.LockdownSettings,
Route.Appearance,
Route.Language,
Route.Display,
Route.PingTarget,
is Route.ConfigGlobal,
Route.Logs -> SETTINGS
is Route.Support,
Route.License,
@@ -113,6 +113,19 @@ fun currentRouteAsNavbarState(
showBottomItems = true,
topTitle = context.getString(R.string.language),
)
LockdownSettings ->
NavbarState(
topLeading = {
IconButton(onClick = { navController.pop() }) {
Icon(
Icons.AutoMirrored.Rounded.ArrowBack,
stringResource(R.string.back),
)
}
},
showBottomItems = true,
topTitle = context.getString(R.string.lockdown_settings),
)
License ->
NavbarState(
topLeading = {
@@ -211,8 +224,11 @@ fun currentRouteAsNavbarState(
}
},
)
is Config -> {
val tunnelName = sharedState.tunnels.find { it.id == route.id }?.name
is Config,
is ConfigGlobal -> {
val tunnelName =
if (route is Config) sharedState.tunnels.find { it.id == route.id }?.name
else context.getString(R.string.global_dns_servers)
NavbarState(
topLeading = {
IconButton(onClick = { navController.pop() }) {
@@ -236,8 +252,12 @@ fun currentRouteAsNavbarState(
},
)
}
is SplitTunnel -> {
val tunnelName = sharedState.tunnels.find { it.id == route.id }?.name
is SplitTunnel,
is SplitTunnelGlobal -> {
val tunnelName =
if (route is SplitTunnel)
sharedState.tunnels.find { it.id == route.id }?.name
else context.getString(R.string.global_split_tunneling)
NavbarState(
topLeading = {
IconButton(onClick = { navController.pop() }) {
@@ -260,52 +280,6 @@ fun currentRouteAsNavbarState(
showBottomItems = true,
)
}
is SplitTunnelGlobal -> {
NavbarState(
topLeading = {
IconButton(onClick = { navController.pop() }) {
Icon(
Icons.AutoMirrored.Rounded.ArrowBack,
stringResource(R.string.back),
)
}
},
topTitle = context.getString(R.string.splt_tunneling),
topTrailing = {
IconButton(
onClick = {
sharedViewModel.postSideEffect(LocalSideEffect.SaveChanges)
}
) {
Icon(Icons.Rounded.Save, stringResource(R.string.save))
}
},
showBottomItems = true,
)
}
is ConfigGlobal -> {
NavbarState(
topLeading = {
IconButton(onClick = { navController.pop() }) {
Icon(
Icons.AutoMirrored.Rounded.ArrowBack,
stringResource(R.string.back),
)
}
},
showBottomItems = true,
topTitle = context.getString(R.string.configuration),
topTrailing = {
IconButton(
onClick = {
sharedViewModel.postSideEffect(LocalSideEffect.SaveChanges)
}
) {
Icon(Icons.Rounded.Save, stringResource(R.string.save))
}
},
)
}
Support ->
NavbarState(
topTitle = context.getString(R.string.support),
@@ -337,7 +311,7 @@ fun currentRouteAsNavbarState(
topTitle = context.getString(R.string.ping_monitor),
showBottomItems = true,
)
is TunnelOptions -> {
is TunnelSettings -> {
val tunnelName = sharedState.tunnels.find { it.id == route.id }?.name
NavbarState(
topLeading = {
@@ -497,20 +471,6 @@ fun currentRouteAsNavbarState(
showBottomItems = true,
)
}
is TunnelGlobals -> {
NavbarState(
topLeading = {
IconButton(onClick = { navController.pop() }) {
Icon(
Icons.AutoMirrored.Rounded.ArrowBack,
stringResource(R.string.back),
)
}
},
topTitle = context.getString(R.string.global_overrides),
showBottomItems = true,
)
}
is WifiPreferences -> {
NavbarState(
topLeading = {
@@ -7,11 +7,12 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.CallSplit
import androidx.compose.material.icons.automirrored.outlined.ViewQuilt
import androidx.compose.material.icons.outlined.*
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
@@ -30,6 +31,7 @@ import com.zaneschepke.wireguardautotunnel.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.SheetButtonWithDivider
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.common.button.SwitchWithDivider
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
@@ -64,14 +66,9 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
val appMode = settingsState.settings.appMode
val dnsEnabled by rememberSaveable(appMode) { mutableStateOf(appMode != AppMode.KERNEL) }
val showProxySettings by
val showModeDivider by
remember(appMode) {
derivedStateOf {
when (appMode) {
AppMode.PROXY -> true
else -> false
}
}
derivedStateOf { appMode == AppMode.PROXY || appMode == AppMode.LOCK_DOWN }
}
fun performBackupRestore(action: () -> Unit) {
@@ -119,11 +116,8 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
leading = {
Icon(ImageVector.vectorResource(R.drawable.sdk), contentDescription = null)
},
trailing = {
Icon(
Icons.Outlined.ExpandMore,
contentDescription = stringResource(R.string.select),
)
trailing = { modifier ->
SheetButtonWithDivider(showModeDivider, modifier) { showAppModeSheet = true }
},
title = stringResource(R.string.backend_mode),
description = {
@@ -131,34 +125,15 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
stringResource(R.string.current_template, appMode.asTitleString(context))
)
},
onClick = { showAppModeSheet = true },
onClick = {
when (appMode) {
AppMode.PROXY -> navController.push(Route.ProxySettings)
AppMode.LOCK_DOWN -> navController.push(Route.LockdownSettings)
AppMode.KERNEL,
AppMode.VPN -> showAppModeSheet = true
}
},
)
if (appMode == AppMode.LOCK_DOWN) {
SurfaceRow(
leading = { Icon(Icons.Outlined.Lan, contentDescription = null) },
title = stringResource(R.string.allow_lan_traffic),
description = {
Text(
text = stringResource(R.string.bypass_lan_for_kill_switch),
style =
MaterialTheme.typography.bodySmall.copy(
MaterialTheme.colorScheme.outline
),
)
},
trailing = {
ScaledSwitch(
checked = settingsState.settings.isLanOnKillSwitchEnabled,
onClick = { viewModel.setLanKillSwitchEnabled(it) },
)
},
onClick = {
viewModel.setLanKillSwitchEnabled(
!settingsState.settings.isLanOnKillSwitchEnabled
)
},
)
}
SurfaceRow(
leading = {
Icon(
@@ -182,29 +157,22 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
)
SurfaceRow(
leading = {
Icon(ImageVector.vectorResource(R.drawable.globe), contentDescription = null)
Icon(Icons.AutoMirrored.Outlined.CallSplit, contentDescription = null)
},
title = stringResource(R.string.global_overrides),
title = stringResource(R.string.global_split_tunneling),
trailing = { modifier ->
SwitchWithDivider(
checked = settingsState.settings.isTunnelGlobalsEnabled,
onClick = { viewModel.setTunnelGlobals(it) },
checked = settingsState.settings.isGlobalSplitTunnelEnabled,
onClick = { viewModel.setGlobalSplitTunneling(it) },
modifier = modifier,
)
},
onClick = {
settingsState.globalTunnelConfig?.let {
navController.push(Route.TunnelGlobals(it.id))
navController.push(Route.SplitTunnelGlobal(id = it.id))
}
},
)
if (showProxySettings) {
SurfaceRow(
leading = { Icon(ImageVector.vectorResource(R.drawable.proxy), null) },
title = stringResource(R.string.proxy_settings),
onClick = { navController.push(Route.ProxySettings) },
)
}
SurfaceRow(
leading = { Icon(Icons.Outlined.Android, null) },
title = stringResource(R.string.android_integrations),
@@ -228,6 +196,10 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
},
title = stringResource(R.string.ping_monitor),
enabled = isPingMonitoringAvailable,
description =
if (!isPingMonitoringAvailable) {
{ DescriptionText(stringResource(R.string.unavailable_in_mode)) }
} else null,
trailing = {
SwitchWithDivider(
checked = settingsState.monitoring.isPingEnabled,
@@ -289,11 +261,13 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
leading = { Icon(Icons.Outlined.SettingsBackupRestore, contentDescription = null) },
title = stringResource(R.string.backup_and_restore),
onClick = { showBackupSheet = true },
trailing = {
Icon(
Icons.Outlined.ExpandMore,
contentDescription = stringResource(R.string.select),
)
trailing = { modifier ->
IconButton(modifier = modifier, onClick = { showBackupSheet = true }) {
Icon(
Icons.Outlined.ExpandMore,
contentDescription = stringResource(R.string.select),
)
}
},
)
}
@@ -4,6 +4,7 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
@@ -12,25 +13,37 @@ import androidx.compose.material.icons.outlined.Dns
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.model.DnsProtocol
import com.zaneschepke.wireguardautotunnel.data.model.DnsProvider
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.common.button.SwitchWithDivider
import com.zaneschepke.wireguardautotunnel.ui.common.dropdown.LabelledDropdown
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
import com.zaneschepke.wireguardautotunnel.util.extensions.capitalize
import com.zaneschepke.wireguardautotunnel.viewmodel.DnsViewModel
import java.util.*
@Composable
fun DnsSettingsScreen(viewModel: DnsViewModel = hiltViewModel()) {
val context = LocalContext.current
val navController = LocalNavController.current
val dnsUiState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
if (dnsUiState.isLoading) return
val locale = remember { Locale.getDefault() }
Column(
horizontalAlignment = Alignment.Start,
@@ -38,6 +51,7 @@ fun DnsSettingsScreen(viewModel: DnsViewModel = hiltViewModel()) {
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()),
) {
Column {
GroupLabel(stringResource(R.string.endpoint), Modifier.padding(horizontal = 16.dp))
LabelledDropdown(
title = stringResource(R.string.dns_protocol),
leading = { Icon(Icons.Outlined.Dns, contentDescription = null) },
@@ -59,5 +73,27 @@ fun DnsSettingsScreen(viewModel: DnsViewModel = hiltViewModel()) {
)
}
}
Column {
GroupLabel(
stringResource(R.string.tunnel).capitalize(locale),
Modifier.padding(horizontal = 16.dp),
)
SurfaceRow(
leading = {
Icon(ImageVector.vectorResource(R.drawable.host), contentDescription = null)
},
title = stringResource(R.string.global_dns_servers),
trailing = { modifier ->
SwitchWithDivider(
checked = dnsUiState.dnsSettings.isGlobalTunnelDnsEnabled,
onClick = { viewModel.setGlobalTunnelDnsEnabled(it) },
modifier = modifier,
)
},
onClick = {
dnsUiState.globalConfig?.let { navController.push(Route.ConfigGlobal(it.id)) }
},
)
}
}
}
@@ -1,46 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.globals
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.CallSplit
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
@Composable
fun TunnelGlobalsScreen(globalTunnelId: Int) {
val navController = LocalNavController.current
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Top),
modifier = Modifier.verticalScroll(rememberScrollState()).fillMaxSize(),
) {
Column {
SurfaceRow(
leading = { Icon(Icons.Outlined.Settings, contentDescription = null) },
title = stringResource(R.string.configuration),
onClick = { navController.push(Route.ConfigGlobal(globalTunnelId)) },
)
SurfaceRow(
leading = {
Icon(Icons.AutoMirrored.Outlined.CallSplit, contentDescription = null)
},
title = stringResource(R.string.splt_tunneling),
onClick = { navController.push(Route.SplitTunnelGlobal(id = globalTunnelId)) },
)
}
}
}
@@ -0,0 +1,94 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.lockdown
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.DataUsage
import androidx.compose.material.icons.outlined.Lan
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.viewmodel.LockdownViewModel
@Composable
fun LockdownSettingsScreen(viewModel: LockdownViewModel = hiltViewModel()) {
val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
if (uiState.isLoading) return
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.Top),
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()),
) {
Column {
GroupLabel(
stringResource(R.string.configuration),
modifier = Modifier.padding(horizontal = 16.dp),
)
SurfaceRow(
leading = { Icon(Icons.Outlined.Lan, contentDescription = null) },
title = stringResource(R.string.allow_lan_traffic),
description = {
Text(
text = stringResource(R.string.bypass_lan_for_kill_switch),
style =
MaterialTheme.typography.bodySmall.copy(
MaterialTheme.colorScheme.outline
),
)
},
trailing = {
ScaledSwitch(
checked = uiState.lockdownSettings.bypassLan,
onClick = { viewModel.setBypassLan(it) },
)
},
onClick = { viewModel.setBypassLan(!uiState.lockdownSettings.bypassLan) },
)
SurfaceRow(
leading = { Icon(Icons.Outlined.DataUsage, contentDescription = null) },
title = stringResource(R.string.metered_tunnel),
trailing = {
ScaledSwitch(
checked = uiState.lockdownSettings.metered,
onClick = { viewModel.setMetered(it) },
)
},
onClick = { viewModel.setMetered(!uiState.lockdownSettings.metered) },
)
SurfaceRow(
leading = {
Icon(ImageVector.vectorResource(R.drawable.host), contentDescription = null)
},
title = "Dual-stack",
trailing = {
ScaledSwitch(
checked = uiState.lockdownSettings.dualStack,
onClick = { viewModel.setDualStack(it) },
)
},
onClick = { viewModel.setDualStack(!uiState.lockdownSettings.dualStack) },
)
}
}
}
@@ -94,7 +94,7 @@ fun TunnelList(
if (sharedState.selectedTunnels.isNotEmpty()) {
viewModel.toggleSelectedTunnel(tunnel.id)
} else {
navController.push(Route.TunnelOptions(tunnel.id))
navController.push(Route.TunnelSettings(tunnel.id))
viewModel.clearSelectedTunnels()
}
},
@@ -165,14 +165,15 @@ fun InterfaceFields(
.lowercase(locale),
modifier = Modifier.weight(3f),
)
ConfigurationTextBox(
value = interfaceState.mtu,
onValueChange = { onInterfaceChange(interfaceState.copy(mtu = it)) },
label = stringResource(R.string.mtu),
hint = stringResource(R.string.auto).lowercase(locale),
modifier = Modifier.weight(2f),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
)
if (!isGlobalConfig)
ConfigurationTextBox(
value = interfaceState.mtu,
onValueChange = { onInterfaceChange(interfaceState.copy(mtu = it)) },
label = stringResource(R.string.mtu),
hint = stringResource(R.string.auto).lowercase(locale),
modifier = Modifier.weight(2f),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
)
}
if (showScripts) {
ConfigurationTextBox(
@@ -70,56 +70,57 @@ fun InterfaceSection(
stringResource(R.string.interface_),
modifier = Modifier.padding(horizontal = 16.dp),
)
Row {
if (isTv && !isGlobalConfig) {
IconButton(onClick = { showPrivateKey = !showPrivateKey }) {
Icon(
Icons.Outlined.RemoveRedEye,
stringResource(R.string.show_password),
)
}
IconButton(
enabled = true,
onClick = {
val keypair = KeyPair()
onInterfaceChange(
configProxy.`interface`.copy(
privateKey = keypair.privateKey.toBase64(),
publicKey = keypair.publicKey.toBase64(),
)
if (!isGlobalConfig)
Row {
if (isTv) {
IconButton(onClick = { showPrivateKey = !showPrivateKey }) {
Icon(
Icons.Outlined.RemoveRedEye,
stringResource(R.string.show_password),
)
},
) {
Icon(
Icons.Rounded.Refresh,
stringResource(R.string.rotate_keys),
tint = MaterialTheme.colorScheme.onSurface,
)
}
IconButton(
enabled = true,
onClick = {
val keypair = KeyPair()
onInterfaceChange(
configProxy.`interface`.copy(
privateKey = keypair.privateKey.toBase64(),
publicKey = keypair.publicKey.toBase64(),
)
)
},
) {
Icon(
Icons.Rounded.Refresh,
stringResource(R.string.rotate_keys),
tint = MaterialTheme.colorScheme.onSurface,
)
}
}
InterfaceDropdown(
expanded = isDropDownExpanded,
onExpandedChange = { isDropDownExpanded = it },
showScripts = showScripts,
showAmneziaValues = showAmneziaValues,
isAmneziaCompatibilitySet = isAmneziaCompatibilitySet,
onToggleScripts = { showScripts = !showScripts },
onToggleAmneziaValues = { showAmneziaValues = !showAmneziaValues },
onToggleAmneziaCompatibility = { toggleAmneziaCompat() },
onMimicQuic = {
showAmneziaValues = true
onMimicQuic()
},
onMimicDns = {
showAmneziaValues = true
onMimicDns()
},
onMimicSip = {
showAmneziaValues = true
onMimicSip()
},
)
}
InterfaceDropdown(
expanded = isDropDownExpanded,
onExpandedChange = { isDropDownExpanded = it },
showScripts = showScripts,
showAmneziaValues = showAmneziaValues,
isAmneziaCompatibilitySet = isAmneziaCompatibilitySet,
onToggleScripts = { showScripts = !showScripts },
onToggleAmneziaValues = { showAmneziaValues = !showAmneziaValues },
onToggleAmneziaCompatibility = { toggleAmneziaCompat() },
onMimicQuic = {
showAmneziaValues = true
onMimicQuic()
},
onMimicDns = {
showAmneziaValues = true
onMimicDns()
},
onMimicSip = {
showAmneziaValues = true
onMimicSip()
},
)
}
}
Column(
verticalArrangement = Arrangement.spacedBy(12.dp),
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.tunneloptions
package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.settings
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -8,6 +8,7 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.CallSplit
import androidx.compose.material.icons.outlined.DataUsage
import androidx.compose.material.icons.outlined.Dns
import androidx.compose.material.icons.outlined.Star
import androidx.compose.material3.Icon
@@ -33,13 +34,13 @@ import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.common.text.DescriptionText
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.tunneloptions.components.QrCodeDialog
import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.settings.components.QrCodeDialog
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
import com.zaneschepke.wireguardautotunnel.viewmodel.TunnelViewModel
import org.orbitmvi.orbit.compose.collectSideEffect
@Composable
fun TunnelOptionsScreen(viewModel: TunnelViewModel) {
fun TunnelSettingsScreen(viewModel: TunnelViewModel) {
val navController = LocalNavController.current
val sharedViewModel = LocalSharedVm.current
@@ -123,10 +124,18 @@ fun TunnelOptionsScreen(viewModel: TunnelViewModel) {
trailing = {
ScaledSwitch(
checked = !tunnel.isIpv4Preferred,
onClick = { viewModel.toggleIpv4Preferred() },
onClick = { viewModel.setIpv4Preferred(!it) },
)
},
onClick = { viewModel.toggleIpv4Preferred() },
onClick = { viewModel.setIpv4Preferred(!tunnel.isIpv4Preferred) },
)
SurfaceRow(
leading = { Icon(Icons.Outlined.DataUsage, contentDescription = null) },
title = stringResource(R.string.metered_tunnel),
trailing = {
ScaledSwitch(checked = tunnel.isMetered, onClick = { viewModel.setMetered(it) })
},
onClick = { viewModel.setMetered(!tunnel.isMetered) },
)
}
}
@@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.tunneloptions.components
package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.settings.components
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
@@ -1,5 +1,10 @@
package com.zaneschepke.wireguardautotunnel.ui.state
import com.zaneschepke.wireguardautotunnel.domain.model.DnsSettings
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
data class DnsUiState(val isLoading: Boolean = true, val dnsSettings: DnsSettings = DnsSettings())
data class DnsUiState(
val isLoading: Boolean = true,
val dnsSettings: DnsSettings = DnsSettings(),
val globalConfig: TunnelConfig? = null,
)
@@ -0,0 +1,8 @@
package com.zaneschepke.wireguardautotunnel.ui.state
import com.zaneschepke.wireguardautotunnel.domain.model.LockdownSettings
data class LockdownSettingsUiState(
val lockdownSettings: LockdownSettings = LockdownSettings(),
val isLoading: Boolean = true,
)
@@ -60,7 +60,8 @@ fun Config.defaultName(): String {
fun Backend.BackendMode.asBackendMode(): BackendMode {
return when (val status = this) {
is Backend.BackendMode.KillSwitch -> BackendMode.KillSwitch(status.allowedIps)
is Backend.BackendMode.KillSwitch ->
BackendMode.KillSwitch(status.allowedIps, status.isMetered, status.isDualStack)
else -> BackendMode.Inactive
}
}
@@ -68,7 +69,8 @@ fun Backend.BackendMode.asBackendMode(): BackendMode {
fun BackendMode.asAmBackendMode(): Backend.BackendMode {
return when (val status = this) {
is BackendMode.Inactive -> Backend.BackendMode.Inactive.INSTANCE
is BackendMode.KillSwitch -> Backend.BackendMode.KillSwitch(status.allowedIps)
is BackendMode.KillSwitch ->
Backend.BackendMode.KillSwitch(status.allowedIps, status.isMetered, status.dualStack)
}
}
@@ -3,25 +3,39 @@ package com.zaneschepke.wireguardautotunnel.viewmodel
import androidx.lifecycle.ViewModel
import com.zaneschepke.wireguardautotunnel.data.model.DnsProtocol
import com.zaneschepke.wireguardautotunnel.data.model.DnsProvider
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.domain.repository.DnsSettingsRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.ui.state.DnsUiState
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.combine
import org.orbitmvi.orbit.ContainerHost
import org.orbitmvi.orbit.viewmodel.container
@HiltViewModel
class DnsViewModel @Inject constructor(private val dnsSettingsRepository: DnsSettingsRepository) :
ContainerHost<DnsUiState, Nothing>, ViewModel() {
class DnsViewModel
@Inject
constructor(
private val dnsSettingsRepository: DnsSettingsRepository,
private val tunnelRepository: TunnelRepository,
) : ContainerHost<DnsUiState, Nothing>, ViewModel() {
override val container =
container<DnsUiState, Nothing>(
DnsUiState(),
buildSettings = { repeatOnSubscribedStopTimeout = 5000L },
) {
dnsSettingsRepository.flow.collect {
reduce { state.copy(dnsSettings = it, isLoading = false) }
}
combine(dnsSettingsRepository.flow, tunnelRepository.globalTunnelFlow) {
dnsSettings,
globalTunnel ->
state.copy(
dnsSettings = dnsSettings,
isLoading = false,
globalConfig = globalTunnel,
)
}
.collect { reduce { it } }
}
fun setDnsProtocol(to: DnsProtocol) = intent {
@@ -35,4 +49,10 @@ class DnsViewModel @Inject constructor(private val dnsSettingsRepository: DnsSet
)
)
}
fun setGlobalTunnelDnsEnabled(to: Boolean) = intent {
dnsSettingsRepository.upsert(state.dnsSettings.copy(isGlobalTunnelDnsEnabled = to))
if (state.globalConfig == null)
tunnelRepository.save(TunnelConfig.generateDefaultGlobalConfig())
}
}
@@ -0,0 +1,38 @@
package com.zaneschepke.wireguardautotunnel.viewmodel
import androidx.lifecycle.ViewModel
import com.zaneschepke.wireguardautotunnel.domain.repository.LockdownSettingsRepository
import com.zaneschepke.wireguardautotunnel.ui.state.LockdownSettingsUiState
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import org.orbitmvi.orbit.ContainerHost
import org.orbitmvi.orbit.viewmodel.container
@HiltViewModel
class LockdownViewModel
@Inject
constructor(private val lockdownSettingsRepository: LockdownSettingsRepository) :
ContainerHost<LockdownSettingsUiState, Nothing>, ViewModel() {
override val container =
container<LockdownSettingsUiState, Nothing>(
LockdownSettingsUiState(),
buildSettings = { repeatOnSubscribedStopTimeout = 5000L },
) {
lockdownSettingsRepository.flow.collect {
reduce { state.copy(lockdownSettings = it, isLoading = false) }
}
}
fun setBypassLan(to: Boolean) = intent {
lockdownSettingsRepository.upsert(state.lockdownSettings.copy(bypassLan = to))
}
fun setMetered(to: Boolean) = intent {
lockdownSettingsRepository.upsert(state.lockdownSettings.copy(metered = to))
}
fun setDualStack(to: Boolean) = intent {
lockdownSettingsRepository.upsert(state.lockdownSettings.copy(dualStack = to))
}
}
@@ -71,12 +71,8 @@ constructor(
settingsRepository.upsert(state.settings.copy(isRestoreOnBootEnabled = to))
}
fun setLanKillSwitchEnabled(to: Boolean) = intent {
settingsRepository.upsert(state.settings.copy(isLanOnKillSwitchEnabled = to))
}
fun setTunnelGlobals(to: Boolean) = intent {
settingsRepository.upsert(state.settings.copy(isTunnelGlobalsEnabled = to))
fun setGlobalSplitTunneling(to: Boolean) = intent {
settingsRepository.upsert(state.settings.copy(isGlobalSplitTunnelEnabled = to))
if (state.globalTunnelConfig == null)
tunnelsRepository.save(TunnelConfig.generateDefaultGlobalConfig())
}
@@ -33,7 +33,7 @@ constructor(
},
tunnelManager.activeTunnels.map { it.containsKey(tunnelId) },
) { tunnel, active ->
state.copy(tunnel = tunnel, isActive = active, isLoading = tunnel == null)
state.copy(tunnel = tunnel, isActive = active, isLoading = false)
}
.collect { reduce { it } }
}
@@ -49,9 +49,14 @@ constructor(
tunnelRepository.updatePrimaryTunnel(update)
}
fun toggleIpv4Preferred() = intent {
val latestTunnel = state.tunnel ?: return@intent
tunnelRepository.save(latestTunnel.copy(isIpv4Preferred = !latestTunnel.isIpv4Preferred))
fun setIpv4Preferred(to: Boolean) = intent {
val tunnel = state.tunnel ?: return@intent
tunnelRepository.save(tunnel.copy(isIpv4Preferred = to))
}
fun setMetered(to: Boolean) = intent {
val tunnel = state.tunnel ?: return@intent
tunnelRepository.save(tunnel.copy(isMetered = to))
}
@AssistedFactory
+5 -1
View File
@@ -375,7 +375,6 @@
the Play Store version of this app. Please browse the project\'s webpages to find where to donate.
</string>
<string name="delete_active_message">Cannot delete active tunnel.</string>
<string name="global_overrides">Global overrides</string>
<string name="back">Back</string>
<string name="configuration">Configuration</string>
<string name="about">About</string>
@@ -418,4 +417,9 @@
<string name="kernel_name_dots">Tunnel name cannot be \'.\' or \'..\' in kernel mode</string>
<string name="kernel_name_special_characters">Tunnel name in kernel mode cannot have spaces or certain special characters (allowed: alphanumeric, _, =, +, ., -)</string>
<string name="tunnel_running_name_message">Name unchangeable while tunnel is active.</string>
<string name="metered_tunnel">Metered tunnel</string>
<string name="lockdown_settings">Lockdown settings</string>
<string name="unavailable_in_mode">Unavailable in this app mode</string>
<string name="global_split_tunneling">Global split tunneling</string>
<string name="global_dns_servers">Global DNS servers</string>
</resources>
+1 -1
View File
@@ -1,7 +1,7 @@
[versions]
accompanist = "0.37.3"
activityCompose = "1.11.0"
amneziawgAndroid = "2.1.13"
amneziawgAndroid = "2.2.0"
androidx-junit = "1.3.0"
icmp4a = "1.0.0"
ipaddress = "5.5.1"