Repository: yggdrasil-network/yggdrasil-android Branch: main Commit: 1b921b313d98 Files: 75 Total size: 185.5 KB Directory structure: gitextract_o5wpfxvr/ ├── .github/ │ └── workflows/ │ └── android.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── app/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ ├── androidTest/ │ │ └── java/ │ │ └── eu/ │ │ └── neilalexander/ │ │ └── yggdrasil/ │ │ └── ExampleInstrumentedTest.kt │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── eu/ │ │ │ └── neilalexander/ │ │ │ └── yggdrasil/ │ │ │ ├── BootUpReceiver.kt │ │ │ ├── ConfigurationProxy.kt │ │ │ ├── DnsActivity.kt │ │ │ ├── GlobalApplication.kt │ │ │ ├── MainActivity.kt │ │ │ ├── NetworkStateCallback.kt │ │ │ ├── PacketTunnelProvider.kt │ │ │ ├── PeersActivity.kt │ │ │ ├── SettingsActivity.kt │ │ │ ├── TileServiceActivity.kt │ │ │ ├── YggStateReceiver.kt │ │ │ └── YggTileService.kt │ │ └── res/ │ │ ├── drawable/ │ │ │ ├── ic_baseline_add_circle_24.xml │ │ │ ├── ic_baseline_chevron_right_24.xml │ │ │ ├── ic_baseline_remove_circle_24.xml │ │ │ ├── ic_tile_icon.xml │ │ │ └── rounded.xml │ │ ├── layout/ │ │ │ ├── activity_dns.xml │ │ │ ├── activity_main.xml │ │ │ ├── activity_peers.xml │ │ │ ├── activity_settings.xml │ │ │ ├── dialog_add_dns_server.xml │ │ │ ├── dialog_addpeer.xml │ │ │ ├── dialog_resetconfig.xml │ │ │ ├── dialog_set_keys.xml │ │ │ ├── dns_server_usable.xml │ │ │ ├── peers_configured.xml │ │ │ └── peers_connected.xml │ │ ├── values/ │ │ │ ├── attrs.xml │ │ │ ├── colors.xml │ │ │ ├── strings.xml │ │ │ ├── styles.xml │ │ │ └── themes.xml │ │ ├── values-night/ │ │ │ └── themes.xml │ │ └── values-ru/ │ │ └── strings.xml │ └── test/ │ └── java/ │ └── eu/ │ └── neilalexander/ │ └── yggdrasil/ │ └── ExampleUnitTest.kt ├── build.gradle ├── fastlane/ │ └── metadata/ │ └── android/ │ ├── en-US/ │ │ ├── changelogs/ │ │ │ ├── 11.txt │ │ │ ├── 15.txt │ │ │ ├── 16.txt │ │ │ ├── 17.txt │ │ │ ├── 18.txt │ │ │ ├── 19.txt │ │ │ ├── 20.txt │ │ │ └── 21.txt │ │ ├── full_description.txt │ │ ├── short_description.txt │ │ └── title.txt │ └── ru/ │ ├── changelogs/ │ │ ├── 11.txt │ │ ├── 15.txt │ │ ├── 16.txt │ │ ├── 17.txt │ │ ├── 18.txt │ │ ├── 19.txt │ │ ├── 20.txt │ │ └── 21.txt │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── readme.md └── settings.gradle ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/android.yml ================================================ name: Build on: push: branches: [ "main" ] pull_request: branches: [ "main" ] workflow_dispatch: release: types: [published] jobs: build: name: Build runs-on: ubuntu-latest steps: - name: Check out uses: actions/checkout@v6 - name: Check out Yggdrasil uses: actions/checkout@v6 with: repository: yggdrasil-network/yggdrasil-go path: yggdrasil-go ref: master fetch-depth: 0 - name: Setup Go environment uses: actions/setup-go@v6 with: go-version: stable - name: Install gomobile run: | go install golang.org/x/mobile/cmd/gomobile@latest ~/go/bin/gomobile init - name: Set up JDK 17 uses: actions/setup-java@v5 with: java-version: '17' distribution: 'temurin' cache: gradle - name: Install NDK uses: nttld/setup-ndk@v1 id: setup-ndk with: ndk-version: r21e add-to-path: false - name: Build Yggdrasil run: | mkdir app/libs cd yggdrasil-go PATH=$PATH:~/go/bin/ ./contrib/mobile/build -a cp {yggdrasil.aar,yggdrasil-sources.jar} ../app/libs env: ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} - name: Gradle build if: github.event_name != 'release' && github.ref_name != 'main' run: | chmod +x gradlew ./gradlew buildRelease - name: Gradle signed build if: github.event_name == 'release' || github.ref_name == 'main' run: | echo "${{ secrets.RELEASE_KEYSTORE }}" > app/gha.keystore.asc gpg -d --passphrase "${{ secrets.RELEASE_KEYSTORE_PASSWORD }}" --batch app/gha.keystore.asc > app/gha.jks chmod +x gradlew ./gradlew assembleYggdrasil - name: Upload build artifact if: github.event_name == 'release' || github.ref_name == 'main' uses: actions/upload-artifact@v7 with: name: yggdrasil-android path: app/build/outputs/apk/yggdrasil/app-yggdrasil.apk - name: Upload release artifact if: github.event_name == 'release' uses: actions/upload-release-asset@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ github.event.release.upload_url }} asset_path: app/build/outputs/apk/yggdrasil/app-yggdrasil.apk asset_name: yggdrasil-android.apk asset_content_type: application/vnd.android.package-archive ================================================ FILE: .gitignore ================================================ *.apk *.iml .gradle /local.properties /.idea/caches /.idea/libraries /.idea/modules.xml /.idea/workspace.xml /.idea/navEditor.xml /.idea/assetWizardSettings.xml .DS_Store /build /captures .externalNativeBuild .cxx local.properties /app/libs/yggdrasil.aar /app/libs/yggdrasil-sources.jar /app/release/* /app/release ================================================ FILE: .gitmodules ================================================ [submodule "libs/yggdrasil-go"] path = libs/yggdrasil-go url = https://github.com/yggdrasil-network/yggdrasil-go ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2021 Yggdrasil Developers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: app/.gitignore ================================================ /build ================================================ FILE: app/build.gradle ================================================ plugins { id 'com.android.application' id 'kotlin-android' } android { compileSdkVersion 34 defaultConfig { applicationId "eu.neilalexander.yggdrasil" minSdkVersion 21 targetSdkVersion 34 versionCode 21 versionName "0.1-021" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } signingConfigs { create("yggdrasil") { // You need to specify either an absolute path or include the // keystore file in the same directory as the build.gradle file. storeFile = file("gha.jks") storePassword = "g1thub4ct10n34yggdr4s1l4ndr01d" keyAlias = "yggdrasil-android" keyPassword = "g1thub4ct10n34yggdr4s1l4ndr01d" } } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } yggdrasil { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' signingConfig = signingConfigs.getByName("yggdrasil") } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = '1.8' } namespace 'eu.neilalexander.yggdrasil' } dependencies { implementation fileTree(include: ['*.aar'], dir: 'libs') implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation 'androidx.core:core-ktx:1.13.1' implementation 'androidx.appcompat:appcompat:1.7.0' implementation 'com.google.android.material:material:1.12.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.preference:preference-ktx:1.2.1' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.2.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' } ================================================ FILE: app/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile ================================================ FILE: app/src/androidTest/java/eu/neilalexander/yggdrasil/ExampleInstrumentedTest.kt ================================================ package eu.neilalexander.yggdrasil import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Test import org.junit.runner.RunWith import org.junit.Assert.* /** * Instrumented test, which will execute on an Android device. * * See [testing documentation](http://d.android.com/tools/testing). */ @RunWith(AndroidJUnit4::class) class ExampleInstrumentedTest { @Test fun useAppContext() { // Context of the app under test. val appContext = InstrumentationRegistry.getInstrumentation().targetContext assertEquals("eu.neilalexander.yggdrasil", appContext.packageName) } } ================================================ FILE: app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app/src/main/java/eu/neilalexander/yggdrasil/BootUpReceiver.kt ================================================ package eu.neilalexander.yggdrasil import android.app.NotificationManager import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.net.VpnService import android.util.Log import androidx.preference.PreferenceManager class BootUpReceiver : BroadcastReceiver() { companion object { const val TAG = "BootUpReceiver" } override fun onReceive(context: Context, intent: Intent) { if (intent.action != Intent.ACTION_BOOT_COMPLETED) { Log.w(TAG, "Wrong action: ${intent.action}") } val preferences = PreferenceManager.getDefaultSharedPreferences(context) if (!preferences.getBoolean(PREF_KEY_ENABLED, false)) { Log.i(TAG, "Yggdrasil disabled, not starting service") return } Log.i(TAG, "Yggdrasil enabled, starting service") val serviceIntent = Intent(context, PacketTunnelProvider::class.java) serviceIntent.action = PacketTunnelProvider.ACTION_START val vpnIntent = VpnService.prepare(context) if (vpnIntent != null) { Log.i(TAG, "Need to ask for VPN permission") val notification = createPermissionMissingNotification(context) val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager manager.notify(444, notification) } else { context.startService(serviceIntent) } } } ================================================ FILE: app/src/main/java/eu/neilalexander/yggdrasil/ConfigurationProxy.kt ================================================ package eu.neilalexander.yggdrasil import android.content.Context import mobile.Mobile import org.json.JSONArray import org.json.JSONObject import java.io.File object ConfigurationProxy { private lateinit var json: JSONObject private lateinit var file: File operator fun invoke(applicationContext: Context): ConfigurationProxy { file = File(applicationContext.filesDir, "yggdrasil.conf") if (!file.exists()) { val conf = Mobile.generateConfigJSON() if (file.createNewFile()) { file.writeBytes(conf) } } fix() return this } fun resetJSON() { val conf = Mobile.generateConfigJSON() file.writeBytes(conf) fix() } fun resetKeys() { val newJson = JSONObject(String(Mobile.generateConfigJSON())) updateJSON { json -> json.put("PrivateKey", newJson.getString("PrivateKey")) } } fun setKeys(privateKey: String) { updateJSON { json -> json.put("PrivateKey", privateKey) } } fun updateJSON(fn: (JSONObject) -> Unit) { json = JSONObject(file.readText(Charsets.UTF_8)) fn(json) val str = json.toString() file.writeText(str, Charsets.UTF_8) } private fun fix() { updateJSON { json -> json.put("AdminListen", "none") json.put("IfName", "none") json.put("IfMTU", 65535) if (json.getJSONArray("MulticastInterfaces").get(0) is String) { val ar = JSONArray() ar.put(0, JSONObject(""" { "Regex": ".*", "Beacon": true, "Listen": true, "Password": "" } """.trimIndent())) json.put("MulticastInterfaces", ar) } } } fun getJSON(): JSONObject { fix() return json } fun getJSONByteArray(): ByteArray { return json.toString().toByteArray(Charsets.UTF_8) } var multicastListen: Boolean get() = (json.getJSONArray("MulticastInterfaces").get(0) as JSONObject).getBoolean("Listen") set(value) { updateJSON { json -> (json.getJSONArray("MulticastInterfaces").get(0) as JSONObject).put("Listen", value) } } var multicastBeacon: Boolean get() = (json.getJSONArray("MulticastInterfaces").get(0) as JSONObject).getBoolean("Beacon") set(value) { updateJSON { json -> (json.getJSONArray("MulticastInterfaces").get(0) as JSONObject).put("Beacon", value) } } var multicastPassword: String get() = (json.getJSONArray("MulticastInterfaces").get(0) as JSONObject).optString("Password") set(value) { updateJSON { json -> (json.getJSONArray("MulticastInterfaces").get(0) as JSONObject).put("Password", value) } } } ================================================ FILE: app/src/main/java/eu/neilalexander/yggdrasil/DnsActivity.kt ================================================ package eu.neilalexander.yggdrasil import android.annotation.SuppressLint import android.app.AlertDialog import android.content.SharedPreferences import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import android.view.ContextThemeWrapper import android.view.LayoutInflater import android.view.View import android.widget.* import androidx.preference.PreferenceManager import com.google.android.material.textfield.TextInputEditText const val KEY_DNS_SERVERS = "dns_servers" const val KEY_DNS_VERSION = "dns_version" const val KEY_ENABLE_CHROME_FIX = "enable_chrome_fix" class DnsActivity : AppCompatActivity() { private lateinit var config: ConfigurationProxy private lateinit var inflater: LayoutInflater private lateinit var serversTableLayout: TableLayout private lateinit var serversTableLabel: TextView private lateinit var serversTableHint: TextView private lateinit var addServerButton: ImageButton private lateinit var enableChromeFix: Switch private lateinit var servers: MutableList private lateinit var preferences: SharedPreferences private lateinit var defaultDnsServers: HashMap> @SuppressLint("ApplySharedPref") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_dns) config = ConfigurationProxy(applicationContext) inflater = LayoutInflater.from(this) val descriptionRevertron = getString(R.string.dns_server_info_revertron) // Here we can add some other DNS servers in a future defaultDnsServers = hashMapOf( "308:62:45:62::" to Pair(getString(R.string.location_amsterdam), descriptionRevertron), "308:84:68:55::" to Pair(getString(R.string.location_frankfurt), descriptionRevertron), "308:25:40:bd::" to Pair(getString(R.string.location_bratislava), descriptionRevertron), "308:c8:48:45::" to Pair(getString(R.string.location_buffalo), descriptionRevertron), ) serversTableLayout = findViewById(R.id.configuredDnsTableLayout) serversTableLabel = findViewById(R.id.configuredDnsLabel) serversTableHint = findViewById(R.id.configuredDnsHint) enableChromeFix = findViewById(R.id.enableChromeFix) addServerButton = findViewById(R.id.addServerButton) addServerButton.setOnClickListener { val view = inflater.inflate(R.layout.dialog_add_dns_server, null) val input = view.findViewById(R.id.addDnsInput) val builder: AlertDialog.Builder = AlertDialog.Builder(ContextThemeWrapper(this, R.style.YggdrasilDialogs)) builder.setTitle(getString(R.string.dns_add_server_dialog_title)) builder.setView(view) builder.setPositiveButton(getString(R.string.add)) { _, _ -> val server = input.text.toString() if (!servers.contains(server)) { servers.add(server) preferences.edit().apply { putString(KEY_DNS_SERVERS, servers.joinToString(",")) commit() } updateConfiguredServers() } else { Toast.makeText(this, R.string.dns_already_added_server, Toast.LENGTH_SHORT).show() } } builder.setNegativeButton(getString(R.string.cancel)) { dialog, _ -> dialog.cancel() } builder.show() } enableChromeFix.setOnCheckedChangeListener { _, isChecked -> preferences.edit().apply { putBoolean(KEY_ENABLE_CHROME_FIX, isChecked) commit() } } val enableChromeFixPanel = findViewById(R.id.enableChromeFixPanel) enableChromeFixPanel.setOnClickListener { enableChromeFix.toggle() } preferences = PreferenceManager.getDefaultSharedPreferences(this.baseContext) val serverString = preferences.getString(KEY_DNS_SERVERS, "") servers = if (serverString!!.isNotEmpty()) { serverString.split(",").toMutableList() } else { mutableListOf() } updateUsableServers() } override fun onResume() { super.onResume() updateConfiguredServers() enableChromeFix.isChecked = preferences.getBoolean(KEY_ENABLE_CHROME_FIX, false) } @SuppressLint("ApplySharedPref") private fun updateConfiguredServers() { when (servers.size) { 0 -> { serversTableLayout.visibility = View.GONE serversTableLabel.text = getString(R.string.dns_no_configured_servers) serversTableHint.text = getText(R.string.dns_configured_servers_hint_empty) } else -> { serversTableLayout.visibility = View.VISIBLE serversTableLabel.text = getString(R.string.dns_configured_servers) serversTableHint.text = getText(R.string.dns_configured_servers_hint) serversTableLayout.removeAllViewsInLayout() for (i in servers.indices) { val server = servers[i] val view = inflater.inflate(R.layout.peers_configured, null) view.findViewById(R.id.addressValue).text = server view.findViewById(R.id.deletePeerButton).tag = i view.findViewById(R.id.deletePeerButton).setOnClickListener { button -> val builder: AlertDialog.Builder = AlertDialog.Builder(ContextThemeWrapper(this, R.style.YggdrasilDialogs)) builder.setTitle(getString(R.string.dns_remove_title, server)) builder.setPositiveButton(getString(R.string.remove)) { dialog, _ -> servers.removeAt(button.tag as Int) preferences.edit().apply { this.putString(KEY_DNS_SERVERS, servers.joinToString(",")) this.commit() } dialog.dismiss() updateConfiguredServers() } builder.setNegativeButton(getString(R.string.cancel)) { dialog, _ -> dialog.cancel() } builder.show() } serversTableLayout.addView(view) } } } } @SuppressLint("ApplySharedPref") private fun updateUsableServers() { val usableTableLayout: TableLayout = findViewById(R.id.usableDnsTableLayout) defaultDnsServers.forEach { val server = it.key val infoPair = it.value val view = inflater.inflate(R.layout.dns_server_usable, null) view.findViewById(R.id.serverValue).text = server val addButton = view.findViewById(R.id.addButton) addButton.tag = server addButton.setOnClickListener { button -> val serverString = button.tag as String if (!servers.contains(serverString)) { servers.add(serverString) preferences.edit().apply { this.putString(KEY_DNS_SERVERS, servers.joinToString(",")) this.commit() } updateConfiguredServers() } else { Toast.makeText(this, R.string.dns_already_added_server, Toast.LENGTH_SHORT).show() } } view.setOnLongClickListener { val builder: AlertDialog.Builder = AlertDialog.Builder(ContextThemeWrapper(this, R.style.YggdrasilDialogs)) builder.setTitle(getString(R.string.dns_server_info_dialog_title)) builder.setMessage("${infoPair.first}\n\n${infoPair.second}") builder.setPositiveButton(getString(R.string.ok)) { dialog, _ -> dialog.dismiss() } builder.show() true } usableTableLayout.addView(view) } } } ================================================ FILE: app/src/main/java/eu/neilalexander/yggdrasil/GlobalApplication.kt ================================================ package eu.neilalexander.yggdrasil import android.app.* import android.content.ComponentName import android.content.Context import android.content.Intent import android.os.Build import android.service.quicksettings.TileService import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat import androidx.preference.PreferenceManager const val PREF_KEY_ENABLED = "enabled" const val PREF_KEY_PEERS_NOTE = "peers_note" const val MAIN_CHANNEL_ID = "Yggdrasil Service" class GlobalApplication: Application(), YggStateReceiver.StateReceiver { private lateinit var config: ConfigurationProxy private var currentState: State = State.Disabled private var updaterConnections: Int = 0 override fun onCreate() { super.onCreate() config = ConfigurationProxy(applicationContext) val callback = NetworkStateCallback(this) callback.register() val receiver = YggStateReceiver(this) receiver.register(this) migrateDnsServers(this) } fun subscribe() { updaterConnections++ } fun unsubscribe() { if (updaterConnections > 0) { updaterConnections-- } } fun needUiUpdates(): Boolean { return updaterConnections > 0 } fun getCurrentState(): State { return currentState } @RequiresApi(Build.VERSION_CODES.N) override fun onStateChange(state: State) { if (state != currentState) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { val componentName = ComponentName(this, YggTileService::class.java) TileService.requestListeningState(this, componentName) } if (state != State.Disabled) { val notification = createServiceNotification(this, state) val notificationManager: NotificationManager = this.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager notificationManager.notify(SERVICE_NOTIFICATION_ID, notification) } currentState = state } } } fun migrateDnsServers(context: Context) { val preferences = PreferenceManager.getDefaultSharedPreferences(context) if (preferences.getInt(KEY_DNS_VERSION, 0) >= 1) { return } val serverString = preferences.getString(KEY_DNS_SERVERS, "") if (serverString!!.isNotEmpty()) { // Replacing old Revertron's servers by new ones val newServers = serverString .replace("300:6223::53", "308:25:40:bd::") .replace("302:7991::53", "308:62:45:62::") .replace("302:db60::53", "308:84:68:55::") .replace("301:1088::53", "308:c8:48:45::") val editor = preferences.edit() editor.putInt(KEY_DNS_VERSION, 1) if (newServers != serverString) { editor.putString(KEY_DNS_SERVERS, newServers) } editor.apply() } } fun createServiceNotification(context: Context, state: State): Notification { createNotificationChannels(context) val intent = Intent(context, MainActivity::class.java).apply { this.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK } var flags = PendingIntent.FLAG_UPDATE_CURRENT if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { flags = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE } val pendingIntent: PendingIntent = PendingIntent.getActivity(context, 0, intent, flags) val text = when (state) { State.Disabled -> context.getText(R.string.tile_disabled) State.Enabled -> context.getText(R.string.tile_enabled) State.Connected -> context.getText(R.string.tile_connected) else -> context.getText(R.string.tile_disabled) } return NotificationCompat.Builder(context, MAIN_CHANNEL_ID) .setShowWhen(false) .setContentTitle(text) .setSmallIcon(R.drawable.ic_tile_icon) .setContentIntent(pendingIntent) .setPriority(NotificationCompat.PRIORITY_MIN) .build() } fun createPermissionMissingNotification(context: Context): Notification { createNotificationChannels(context) val intent = Intent(context, MainActivity::class.java).apply { this.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK } var flags = PendingIntent.FLAG_UPDATE_CURRENT if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { flags = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE } val pendingIntent: PendingIntent = PendingIntent.getActivity(context, 0, intent, flags) return NotificationCompat.Builder(context, MAIN_CHANNEL_ID) .setShowWhen(false) .setContentTitle(context.getText(R.string.app_name)) .setContentText(context.getText(R.string.permission_notification_text)) .setSmallIcon(R.drawable.ic_tile_icon) .setContentIntent(pendingIntent) .setAutoCancel(true) .setPriority(NotificationCompat.PRIORITY_DEFAULT) .build() } private fun createNotificationChannels(context: Context) { // Create the NotificationChannel, but only on API 26+ because // the NotificationChannel class is new and not in the support library if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val name = context.getString(R.string.channel_name) val descriptionText = context.getString(R.string.channel_description) val importance = NotificationManager.IMPORTANCE_MIN val channel = NotificationChannel(MAIN_CHANNEL_ID, name, importance).apply { description = descriptionText } // Register the channel with the system val notificationManager: NotificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager notificationManager.createNotificationChannel(channel) } } ================================================ FILE: app/src/main/java/eu/neilalexander/yggdrasil/MainActivity.kt ================================================ package eu.neilalexander.yggdrasil import android.app.Activity import android.app.AlertDialog import android.content.* import android.graphics.Color import android.net.Uri import android.net.VpnService import android.os.Bundle import android.view.ContextThemeWrapper import android.widget.Switch import android.widget.TextView import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.LinearLayoutCompat import androidx.core.content.edit import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.preference.PreferenceManager import eu.neilalexander.yggdrasil.PacketTunnelProvider.Companion.STATE_INTENT import mobile.Mobile import org.json.JSONArray const val APP_WEB_URL = "https://github.com/yggdrasil-network/yggdrasil-android" class MainActivity : AppCompatActivity() { private lateinit var enabledSwitch: Switch private lateinit var enabledLabel: TextView private lateinit var ipAddressLabel: TextView private lateinit var subnetLabel: TextView private lateinit var peersLabel: TextView private lateinit var peersRow: LinearLayoutCompat private lateinit var dnsLabel: TextView private lateinit var dnsRow: LinearLayoutCompat private lateinit var settingsRow: LinearLayoutCompat private lateinit var versionRow: LinearLayoutCompat private fun start() { val intent = Intent(this, PacketTunnelProvider::class.java) intent.action = PacketTunnelProvider.ACTION_START startService(intent) } private var startVpnActivity = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode == Activity.RESULT_OK) { start() } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) findViewById(R.id.versionValue).text = Mobile.getVersion() enabledSwitch = findViewById(R.id.enableYggdrasil) enabledLabel = findViewById(R.id.yggdrasilStatusLabel) ipAddressLabel = findViewById(R.id.ipAddressValue) subnetLabel = findViewById(R.id.subnetValue) peersLabel = findViewById(R.id.peersValue) peersRow = findViewById(R.id.peersTableRow) dnsLabel = findViewById(R.id.dnsValue) dnsRow = findViewById(R.id.dnsTableRow) settingsRow = findViewById(R.id.settingsTableRow) versionRow = findViewById(R.id.versionTableRow) enabledLabel.setTextColor(Color.GRAY) enabledSwitch.setOnCheckedChangeListener { _, isChecked -> when (isChecked) { true -> { val vpnIntent = VpnService.prepare(this) if (vpnIntent != null) { startVpnActivity.launch(vpnIntent) } else { start() enabledSwitch.isEnabled = false } } false -> { val intent = Intent(this, PacketTunnelProvider::class.java) intent.action = PacketTunnelProvider.ACTION_STOP startService(intent) } } val preferences = PreferenceManager.getDefaultSharedPreferences(this.baseContext) preferences.edit(commit = true) { putBoolean(PREF_KEY_ENABLED, isChecked) } } val enableYggdrasilPanel = findViewById(R.id.enableYggdrasilPanel) enableYggdrasilPanel.setOnClickListener { enabledSwitch.toggle() } peersRow.isClickable = true peersRow.setOnClickListener { val intent = Intent(this, PeersActivity::class.java) startActivity(intent) } dnsRow.isClickable = true dnsRow.setOnClickListener { val intent = Intent(this, DnsActivity::class.java) startActivity(intent) } settingsRow.isClickable = true settingsRow.setOnClickListener { val intent = Intent(this, SettingsActivity::class.java) startActivity(intent) } versionRow.isClickable = true versionRow.setOnClickListener { openUrlInBrowser(APP_WEB_URL) } ipAddressLabel.setOnLongClickListener { val clipboard: ClipboardManager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager val clip = ClipData.newPlainText("ip", ipAddressLabel.text) clipboard.setPrimaryClip(clip) Toast.makeText(applicationContext,R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show() true } subnetLabel.setOnLongClickListener { val clipboard: ClipboardManager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager val clip = ClipData.newPlainText("subnet", subnetLabel.text) clipboard.setPrimaryClip(clip) Toast.makeText(applicationContext,R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show() true } } override fun onResume() { super.onResume() LocalBroadcastManager.getInstance(this).registerReceiver( receiver, IntentFilter(STATE_INTENT) ) val preferences = PreferenceManager.getDefaultSharedPreferences(this.baseContext) enabledSwitch.isChecked = preferences.getBoolean(PREF_KEY_ENABLED, false) val serverString = preferences.getString(KEY_DNS_SERVERS, "") if (serverString!!.isNotEmpty()) { val servers = serverString.split(",") dnsLabel.text = when (servers.size) { 0 -> getString(R.string.dns_no_servers) 1 -> getString(R.string.dns_one_server) else -> getString(R.string.dns_many_servers, servers.size) } } else { dnsLabel.text = getString(R.string.dns_no_servers) } (application as GlobalApplication).subscribe() } override fun onPause() { super.onPause() (application as GlobalApplication).unsubscribe() LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver) } private val receiver: BroadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent) { when (intent.getStringExtra("type")) { "state" -> { val peerState = JSONArray(intent.getStringExtra("peers") ?: "[]") var count = 0 for (i in 0.. getString(R.string.main_no_peers) 1 -> getString(R.string.main_one_peer) else -> getString(R.string.main_many_peers, count) } } if (!enabledSwitch.isEnabled) { enabledSwitch.isEnabled = true } } } } } private fun showPeersNoteIfNeeded(peerCount: Int) { if (peerCount > 0) return val preferences = PreferenceManager.getDefaultSharedPreferences(this@MainActivity.baseContext) if (!preferences.getBoolean(PREF_KEY_PEERS_NOTE, false)) { this@MainActivity.runOnUiThread { val builder: AlertDialog.Builder = AlertDialog.Builder(ContextThemeWrapper(this@MainActivity, R.style.YggdrasilDialogs)) builder.setTitle(getString(R.string.main_add_some_peers_title)) builder.setMessage(getString(R.string.main_add_some_peers_message)) builder.setPositiveButton(getString(R.string.ok)) { dialog, _ -> dialog.dismiss() } builder.show() } // Mark this note as shown preferences.edit().apply { putBoolean(PREF_KEY_PEERS_NOTE, true) commit() } } } fun openUrlInBrowser(url: String) { val intent = Intent(Intent.ACTION_VIEW).apply { data = Uri.parse(url) } try { startActivity(intent) } catch (e: ActivityNotFoundException) { // Handle the exception if no browser is found Toast.makeText(this, getText(R.string.no_browser_found_toast), Toast.LENGTH_SHORT).show() } } } ================================================ FILE: app/src/main/java/eu/neilalexander/yggdrasil/NetworkStateCallback.kt ================================================ package eu.neilalexander.yggdrasil import android.content.Context import android.content.Intent import android.net.* import android.os.Build import android.util.Log import androidx.preference.PreferenceManager private const val TAG = "Network" class NetworkStateCallback(val context: Context) : ConnectivityManager.NetworkCallback() { override fun onAvailable(network: Network) { super.onAvailable(network) Log.d(TAG, "onAvailable") val preferences = PreferenceManager.getDefaultSharedPreferences(context) if (preferences.getBoolean(PREF_KEY_ENABLED, false)) { Thread { // The message often arrives before the connection is fully established Thread.sleep(1000) val intent = Intent(context, PacketTunnelProvider::class.java) intent.action = PacketTunnelProvider.ACTION_CONNECT try { context.startService(intent) } catch (e: IllegalStateException) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { context.startForegroundService(intent) } } }.start() } } override fun onLost(network: Network) { super.onLost(network) Log.d(TAG, "onLost") } fun register() { val request = NetworkRequest.Builder() .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET) .build() val manager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager manager.registerNetworkCallback(request, this) } } ================================================ FILE: app/src/main/java/eu/neilalexander/yggdrasil/PacketTunnelProvider.kt ================================================ package eu.neilalexander.yggdrasil import android.content.Intent import android.net.VpnService import android.net.wifi.WifiManager import android.os.Build import android.os.ParcelFileDescriptor import android.system.OsConstants import android.util.Log import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.preference.PreferenceManager import eu.neilalexander.yggdrasil.YggStateReceiver.Companion.YGG_STATE_INTENT import mobile.Yggdrasil import org.json.JSONArray import java.io.FileInputStream import java.io.FileOutputStream import java.util.concurrent.atomic.AtomicBoolean import kotlin.concurrent.thread private const val TAG = "PacketTunnelProvider" const val SERVICE_NOTIFICATION_ID = 1000 open class PacketTunnelProvider: VpnService() { companion object { const val STATE_INTENT = "eu.neilalexander.yggdrasil.PacketTunnelProvider.STATE_MESSAGE" const val ACTION_START = "eu.neilalexander.yggdrasil.PacketTunnelProvider.START" const val ACTION_STOP = "eu.neilalexander.yggdrasil.PacketTunnelProvider.STOP" const val ACTION_TOGGLE = "eu.neilalexander.yggdrasil.PacketTunnelProvider.TOGGLE" const val ACTION_CONNECT = "eu.neilalexander.yggdrasil.PacketTunnelProvider.CONNECT" } private var yggdrasil = Yggdrasil() private var started = AtomicBoolean() private lateinit var config: ConfigurationProxy private var readerThread: Thread? = null private var writerThread: Thread? = null private var updateThread: Thread? = null private var parcel: ParcelFileDescriptor? = null private var readerStream: FileInputStream? = null private var writerStream: FileOutputStream? = null private var multicastLock: WifiManager.MulticastLock? = null override fun onCreate() { super.onCreate() config = ConfigurationProxy(applicationContext) } override fun onDestroy() { super.onDestroy() stop() } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { if (intent == null) { Log.d(TAG, "Intent is null") return START_NOT_STICKY } val preferences = PreferenceManager.getDefaultSharedPreferences(this.baseContext) val enabled = preferences.getBoolean(PREF_KEY_ENABLED, false) return when (intent.action ?: ACTION_STOP) { ACTION_STOP -> { Log.d(TAG, "Stopping...") stop(); START_NOT_STICKY } ACTION_CONNECT -> { Log.d(TAG, "Connecting...") if (started.get()) { connect() } else { start() } START_STICKY } ACTION_TOGGLE -> { Log.d(TAG, "Toggling...") if (started.get()) { stop(); START_NOT_STICKY } else { start(); START_STICKY } } else -> { if (!enabled) { Log.d(TAG, "Service is disabled") return START_NOT_STICKY } Log.d(TAG, "Starting...") start(); START_STICKY } } } private fun start() { if (!started.compareAndSet(false, true)) { return } val notification = createServiceNotification(this, State.Enabled) startForeground(SERVICE_NOTIFICATION_ID, notification) // Acquire multicast lock val wifi = applicationContext.getSystemService(WIFI_SERVICE) as WifiManager multicastLock = wifi.createMulticastLock("Yggdrasil").apply { setReferenceCounted(false) acquire() } Log.d(TAG, config.getJSON().toString()) yggdrasil.startJSON(config.getJSONByteArray()) val address = yggdrasil.addressString val builder = Builder() .addAddress(address, 7) .addRoute("200::", 7) // We do this to trick the DNS-resolver into thinking that we have "regular" IPv6, // and therefore we need to resolve AAAA DNS-records. // See: https://android.googlesource.com/platform/bionic/+/refs/heads/master/libc/dns/net/getaddrinfo.c#1935 // and: https://android.googlesource.com/platform/bionic/+/refs/heads/master/libc/dns/net/getaddrinfo.c#365 // If we don't do this the DNS-resolver just doesn't do DNS-requests with record type AAAA, // and we can't use DNS with Yggdrasil addresses. .addRoute("2000::", 128) .allowFamily(OsConstants.AF_INET) .allowBypass() .setBlocking(true) .setMtu(yggdrasil.mtu.toInt()) .setSession("Yggdrasil") // On Android API 29+ apps can opt-in/out to using metered networks. // If we don't set metered status of VPN it is considered as metered. // If we set it to false, then it will inherit this status from underlying network. // See: https://developer.android.com/reference/android/net/VpnService.Builder#setMetered(boolean) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { builder.setMetered(false) } val preferences = PreferenceManager.getDefaultSharedPreferences(this.baseContext) val serverString = preferences.getString(KEY_DNS_SERVERS, "") if (serverString!!.isNotEmpty()) { val servers = serverString.split(",") if (servers.isNotEmpty()) { servers.forEach { Log.i(TAG, "Using DNS server $it") builder.addDnsServer(it) } } } if (preferences.getBoolean(KEY_ENABLE_CHROME_FIX, false)) { builder.addRoute("2001:4860:4860::8888", 128) } parcel = builder.establish() val parcel = parcel if (parcel == null || !parcel.fileDescriptor.valid()) { stop() return } readerStream = FileInputStream(parcel.fileDescriptor) writerStream = FileOutputStream(parcel.fileDescriptor) readerThread = thread { reader() } writerThread = thread { writer() } updateThread = thread { updater() } var intent = Intent(YGG_STATE_INTENT) intent.putExtra("state", STATE_ENABLED) LocalBroadcastManager.getInstance(this).sendBroadcast(intent) } private fun stop() { if (!started.compareAndSet(true, false)) { return } yggdrasil.stop() readerStream?.let { it.close() readerStream = null } writerStream?.let { it.close() writerStream = null } parcel?.let { it.close() parcel = null } readerThread?.let { it.interrupt() readerThread = null } writerThread?.let { it.interrupt() writerThread = null } updateThread?.let { it.interrupt() updateThread = null } var intent = Intent(STATE_INTENT) intent.putExtra("type", "state") intent.putExtra("started", false) LocalBroadcastManager.getInstance(this).sendBroadcast(intent) intent = Intent(YGG_STATE_INTENT) intent.putExtra("state", STATE_DISABLED) LocalBroadcastManager.getInstance(this).sendBroadcast(intent) stopForeground(true) stopSelf() multicastLock?.release() } private fun connect() { if (!started.get()) { return } yggdrasil.retryPeersNow() } private fun updater() { try { Thread.sleep(500) } catch (_: InterruptedException) { return } var lastStateUpdate = System.currentTimeMillis() updates@ while (started.get()) { val treeJSON = yggdrasil.treeJSON if ((application as GlobalApplication).needUiUpdates()) { val intent = Intent(STATE_INTENT) intent.putExtra("type", "state") intent.putExtra("started", true) intent.putExtra("ip", yggdrasil.addressString) intent.putExtra("subnet", yggdrasil.subnetString) intent.putExtra("pubkey", yggdrasil.publicKeyString) intent.putExtra("peers", yggdrasil.peersJSON) LocalBroadcastManager.getInstance(this).sendBroadcast(intent) } val curTime = System.currentTimeMillis() if (lastStateUpdate + 10000 < curTime) { val intent = Intent(YGG_STATE_INTENT) var state = STATE_ENABLED if (yggdrasil.routingEntries > 0) { state = STATE_CONNECTED } if (treeJSON != null && treeJSON != "null") { val treeState = JSONArray(treeJSON) val count = treeState.length() if (count > 1) state = STATE_CONNECTED } intent.putExtra("state", state) LocalBroadcastManager.getInstance(this).sendBroadcast(intent) lastStateUpdate = curTime } if (Thread.currentThread().isInterrupted) { break@updates } if (sleep()) return } } private fun sleep(): Boolean { try { Thread.sleep(1000) } catch (e: InterruptedException) { return true } return false } private fun writer() { val buf = ByteArray(65535) writes@ while (started.get()) { val writerStream = writerStream val writerThread = writerThread if (writerThread == null || writerStream == null) { Log.i(TAG, "Write thread or stream is null") break@writes } if (Thread.currentThread().isInterrupted || !writerStream.fd.valid()) { Log.i(TAG, "Write thread interrupted or file descriptor is invalid") break@writes } try { val len = yggdrasil.recvBuffer(buf) if (len > 0) { writerStream.write(buf, 0, len.toInt()) } } catch (e: Exception) { Log.i(TAG, "Error in write: $e") if (e.toString().contains("ENOBUFS")) { //TODO Check this by some error code //More info about this: https://github.com/AdguardTeam/AdguardForAndroid/issues/724 continue } break@writes } } writerStream?.let { it.close() writerStream = null } } private fun reader() { val b = ByteArray(65535) reads@ while (started.get()) { val readerStream = readerStream val readerThread = readerThread if (readerThread == null || readerStream == null) { Log.i(TAG, "Read thread or stream is null") break@reads } if (Thread.currentThread().isInterrupted ||!readerStream.fd.valid()) { Log.i(TAG, "Read thread interrupted or file descriptor is invalid") break@reads } try { val n = readerStream.read(b) yggdrasil.sendBuffer(b, n.toLong()) } catch (e: Exception) { Log.i(TAG, "Error in sendBuffer: $e") break@reads } } readerStream?.let { it.close() readerStream = null } } } ================================================ FILE: app/src/main/java/eu/neilalexander/yggdrasil/PeersActivity.kt ================================================ package eu.neilalexander.yggdrasil import android.app.AlertDialog import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.os.Bundle import android.text.method.LinkMovementMethod import android.util.Log import android.view.ContextThemeWrapper import android.view.KeyEvent import android.view.LayoutInflater import android.view.View import android.widget.EditText import android.widget.ImageButton import android.widget.Switch import android.widget.TableLayout import android.widget.TableRow import android.widget.TextView import androidx.appcompat.app.AppCompatActivity import androidx.core.widget.doOnTextChanged import androidx.localbroadcastmanager.content.LocalBroadcastManager import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputLayout import org.json.JSONArray import org.json.JSONObject import java.net.URI class PeersActivity : AppCompatActivity() { private lateinit var config: ConfigurationProxy private lateinit var inflater: LayoutInflater private lateinit var peers: Array private lateinit var connectedTableLayout: TableLayout private lateinit var connectedTableLabel: TextView private lateinit var configuredTableLayout: TableLayout private lateinit var configuredTableLabel: TextView private lateinit var multicastListenSwitch: Switch private lateinit var multicastBeaconSwitch: Switch private lateinit var passwordEdit: EditText private lateinit var addPeerButton: ImageButton override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_peers) config = ConfigurationProxy(applicationContext) inflater = LayoutInflater.from(this) peers = emptyArray() connectedTableLayout = findViewById(R.id.connectedPeersTableLayout) connectedTableLabel = findViewById(R.id.connectedPeersLabel) configuredTableLayout = findViewById(R.id.configuredPeersTableLayout) configuredTableLabel = findViewById(R.id.configuredPeersLabel) val discoveryLink = findViewById(R.id.peers_discovery_link) discoveryLink.movementMethod = LinkMovementMethod.getInstance() multicastListenSwitch = findViewById(R.id.enableMulticastListen) multicastListenSwitch.setOnCheckedChangeListener { button, _ -> config.multicastListen = button.isChecked } multicastBeaconSwitch = findViewById(R.id.enableMulticastBeacon) multicastBeaconSwitch.setOnCheckedChangeListener { button, _ -> config.multicastBeacon = button.isChecked } multicastListenSwitch.isChecked = config.multicastListen multicastBeaconSwitch.isChecked = config.multicastBeacon val multicastBeaconPanel = findViewById(R.id.enableMulticastBeaconPanel) multicastBeaconPanel.setOnClickListener { multicastBeaconSwitch.toggle() } val multicastListenPanel = findViewById(R.id.enableMulticastListenPanel) multicastListenPanel.setOnClickListener { multicastListenSwitch.toggle() } passwordEdit = findViewById(R.id.passwordEdit) passwordEdit.setText(config.multicastPassword) passwordEdit.doOnTextChanged { text, _, _, _ -> config.multicastPassword = text.toString() } passwordEdit.setOnKeyListener { _, keyCode, _ -> (keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER) } findViewById(R.id.passwordTableRow).setOnKeyListener { _, keyCode, event -> Log.i("Key", keyCode.toString()) if (event.action == KeyEvent.ACTION_DOWN) { if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER) { passwordEdit.requestFocus() true } else { false } } else { false } } addPeerButton = findViewById(R.id.addPeerButton) addPeerButton.setOnClickListener { val view = inflater.inflate(R.layout.dialog_addpeer, null) val input = view.findViewById(R.id.addPeerInput) val inputLayout = view.findViewById(R.id.addPeerInputLayout) val builder: AlertDialog.Builder = AlertDialog.Builder(ContextThemeWrapper(this, R.style.YggdrasilDialogs)) builder.setTitle(getString(R.string.peers_add_peer)) builder.setView(view) builder.setPositiveButton(getString(R.string.peers_add)) { dialog, _ -> config.updateJSON { json -> json.getJSONArray("Peers").put(input.text.toString().trim()) } dialog.dismiss() updateConfiguredPeers() } builder.setNegativeButton(getString(R.string.cancel)) { dialog, _ -> dialog.cancel() } val dialog = builder.create() dialog.show() val addButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE) addButton.isEnabled = false input.doOnTextChanged { text, _, _, _ -> val error = validatePeerUri(text.toString().trim()) inputLayout?.error = error addButton.isEnabled = error == null && !text.isNullOrBlank() } } } override fun onResume() { super.onResume() LocalBroadcastManager.getInstance(this).registerReceiver( receiver, IntentFilter(PacketTunnelProvider.STATE_INTENT) ) (application as GlobalApplication).subscribe() updateConfiguredPeers() updateConnectedPeers() } override fun onPause() { super.onPause() (application as GlobalApplication).unsubscribe() LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver) } private fun updateConfiguredPeers() { val peers = config.getJSON().getJSONArray("Peers") when (peers.length()) { 0 -> { configuredTableLayout.visibility = View.GONE configuredTableLabel.text = getString(R.string.peers_no_configured_title) } else -> { configuredTableLayout.visibility = View.VISIBLE configuredTableLabel.text = getString(R.string.peers_configured_title) configuredTableLayout.removeAllViewsInLayout() for (i in 0 until peers.length()) { val peer = peers[i].toString() val view = inflater.inflate(R.layout.peers_configured, null) view.findViewById(R.id.addressValue).text = peer view.findViewById(R.id.deletePeerButton).tag = i view.findViewById(R.id.deletePeerButton).setOnClickListener { button -> val builder: AlertDialog.Builder = AlertDialog.Builder(ContextThemeWrapper(this, R.style.YggdrasilDialogs)) builder.setTitle(getString(R.string.peers_remove_title, peer)) builder.setPositiveButton(getString(R.string.peers_remove)) { dialog, _ -> config.updateJSON { json -> json.getJSONArray("Peers").remove(button.tag as Int) } dialog.dismiss() updateConfiguredPeers() } builder.setNegativeButton(getString(R.string.cancel)) { dialog, _ -> dialog.cancel() } builder.show() } configuredTableLayout.addView(view) } } } } private fun updateConnectedPeers() { when (peers.size) { 0 -> { connectedTableLayout.visibility = View.GONE connectedTableLabel.text = getString(R.string.peers_no_connected_title) } else -> { var connected = false connectedTableLayout.removeAllViewsInLayout() for (peer in peers) { val view = inflater.inflate(R.layout.peers_connected, null) val ip = peer.getString("IP") // Only connected peers have IPs if (ip.isNotEmpty()) { view.findViewById(R.id.addressLabel).text = ip view.findViewById(R.id.detailsLabel).text = peer.getString("URI") connectedTableLayout.addView(view) connected = true } } if (connected) { connectedTableLayout.visibility = View.VISIBLE connectedTableLabel.text = getString(R.string.peers_connected_title) } else { connectedTableLayout.visibility = View.GONE connectedTableLabel.text = getString(R.string.peers_no_connected_title) } } } } private val validSchemes = setOf("tcp", "tls", "quic", "ws", "wss", "socks") private fun validatePeerUri(input: String): String? { if (input.isEmpty()) return null val uri = try { URI(input) } catch (e: Exception) { return getString(R.string.peer_invalid_uri) } val scheme = uri.scheme?.lowercase() if (scheme == null || scheme !in validSchemes) { return getString(R.string.peer_invalid_scheme, validSchemes.joinToString(", ")) } if (scheme != "socks") { if (uri.host.isNullOrEmpty()) { return getString(R.string.peer_missing_host) } if (uri.port == -1) { return getString(R.string.peer_missing_port) } } return null } private val receiver: BroadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent) { when (intent.getStringExtra("type")) { "state" -> { if (intent.hasExtra("peers")) { val peers1 = intent.getStringExtra("peers") //Log.i("PeersActivity", "Peers json: $peers1") val peersArray = JSONArray(peers1 ?: "[]") val array = Array(peersArray.length()) { i -> peersArray.getJSONObject(i) } array.sortWith(compareBy { it.getString("IP") }) peers = array updateConnectedPeers() } } } } } } ================================================ FILE: app/src/main/java/eu/neilalexander/yggdrasil/SettingsActivity.kt ================================================ package eu.neilalexander.yggdrasil import android.app.AlertDialog import android.content.BroadcastReceiver import android.content.ClipData import android.content.ClipboardManager import android.content.Context import android.content.Intent import android.content.IntentFilter import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import android.util.Log import android.view.ContextThemeWrapper import android.view.KeyEvent import android.view.LayoutInflater import android.view.View import android.widget.* import androidx.appcompat.widget.LinearLayoutCompat import androidx.core.widget.doOnTextChanged import androidx.localbroadcastmanager.content.LocalBroadcastManager import org.json.JSONObject class SettingsActivity : AppCompatActivity() { private lateinit var config: ConfigurationProxy private lateinit var inflater: LayoutInflater private lateinit var deviceNameEntry: EditText private lateinit var publicKeyLabel: TextView private lateinit var resetConfigurationRow: LinearLayoutCompat private var publicKeyReset = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_settings) config = ConfigurationProxy(applicationContext) inflater = LayoutInflater.from(this) deviceNameEntry = findViewById(R.id.deviceNameEntry) publicKeyLabel = findViewById(R.id.publicKeyLabel) resetConfigurationRow = findViewById(R.id.resetConfigurationRow) deviceNameEntry.doOnTextChanged { text, _, _, _ -> config.updateJSON { cfg -> val nodeInfo = cfg.optJSONObject("NodeInfo") if (nodeInfo == null) { cfg.put("NodeInfo", JSONObject("{}")) } cfg.getJSONObject("NodeInfo").put("name", text) } } deviceNameEntry.setOnKeyListener { view, keyCode, event -> (keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER) } findViewById(R.id.deviceNameTableRow).setOnKeyListener { view, keyCode, event -> Log.i("Key", keyCode.toString()) if (event.action == KeyEvent.ACTION_DOWN) { if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER) { deviceNameEntry.requestFocus() true } else { false } } else { false } } resetConfigurationRow.setOnClickListener { val view = inflater.inflate(R.layout.dialog_resetconfig, null) val builder: AlertDialog.Builder = AlertDialog.Builder(ContextThemeWrapper(this, R.style.YggdrasilDialogs)) builder.setTitle(getString(R.string.settings_warning_title)) builder.setView(view) builder.setPositiveButton(getString(R.string.settings_reset)) { dialog, _ -> config.resetJSON() updateView() dialog.dismiss() } builder.setNegativeButton(getString(R.string.cancel)) { dialog, _ -> dialog.cancel() } builder.show() } findViewById(R.id.resetKeysRow).setOnClickListener { config.resetKeys() publicKeyReset = true updateView() } findViewById(R.id.setKeysRow).setOnClickListener { val view = inflater.inflate(R.layout.dialog_set_keys, null) val builder: AlertDialog.Builder = AlertDialog.Builder(ContextThemeWrapper(this, R.style.YggdrasilDialogs)) val privateKey = view.findViewById(R.id.private_key) builder.setTitle(getString(R.string.set_keys)) builder.setView(view) builder.setPositiveButton(getString(R.string.save)) { dialog, _ -> config.setKeys(privateKey.text.toString()) updateView() dialog.dismiss() } builder.setNegativeButton(getString(R.string.cancel)) { dialog, _ -> dialog.cancel() } builder.show() } publicKeyLabel.setOnLongClickListener { val clipboard: ClipboardManager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager val clip = ClipData.newPlainText("public key", publicKeyLabel.text) clipboard.setPrimaryClip(clip) Toast.makeText(applicationContext,R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show() true } updateView() } private fun updateView() { val json = config.getJSON() val nodeinfo = json.optJSONObject("NodeInfo") if (nodeinfo != null) { deviceNameEntry.setText(nodeinfo.getString("name"), TextView.BufferType.EDITABLE) } else { deviceNameEntry.setText("", TextView.BufferType.EDITABLE) } var key = json.optString("PrivateKey") if (key.isNotEmpty()) { key = key.substring(key.length / 2) } publicKeyLabel.text = key } override fun onResume() { super.onResume() LocalBroadcastManager.getInstance(this).registerReceiver( receiver, IntentFilter(PacketTunnelProvider.STATE_INTENT) ) (application as GlobalApplication).subscribe() } override fun onPause() { super.onPause() (application as GlobalApplication).unsubscribe() LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver) } // To be able to get public key from running Yggdrasil we use this receiver, as we don't have this field in config private val receiver: BroadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent) { if (intent.hasExtra("pubkey") && !publicKeyReset) { val tree = intent.getStringExtra("pubkey") if (tree != null && tree != "null") { publicKeyLabel.text = intent.getStringExtra("pubkey") } } } } } ================================================ FILE: app/src/main/java/eu/neilalexander/yggdrasil/TileServiceActivity.kt ================================================ package eu.neilalexander.yggdrasil import android.app.Activity import android.content.Intent import android.os.Bundle class TileServiceActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Just starting MainActivity val intent = Intent(this, MainActivity::class.java) startService(intent) finish() } } ================================================ FILE: app/src/main/java/eu/neilalexander/yggdrasil/YggStateReceiver.kt ================================================ package eu.neilalexander.yggdrasil import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import androidx.localbroadcastmanager.content.LocalBroadcastManager const val STATE_ENABLED = "enabled" const val STATE_DISABLED = "disabled" const val STATE_CONNECTED = "connected" const val STATE_RECONNECTING = "reconnecting" class YggStateReceiver(var receiver: StateReceiver): BroadcastReceiver() { companion object { const val YGG_STATE_INTENT = "eu.neilalexander.yggdrasil.YggStateReceiver.STATE" } override fun onReceive(context: Context?, intent: Intent?) { if (context == null) return val state = when (intent?.getStringExtra("state")) { STATE_ENABLED -> State.Enabled STATE_DISABLED -> State.Disabled STATE_CONNECTED -> State.Connected STATE_RECONNECTING -> State.Reconnecting else -> State.Unknown } receiver.onStateChange(state) } fun register(context: Context) { LocalBroadcastManager.getInstance(context).registerReceiver( this, IntentFilter(YGG_STATE_INTENT) ) } fun unregister(context: Context) { LocalBroadcastManager.getInstance(context).unregisterReceiver(this) } interface StateReceiver { fun onStateChange(state: State) } } /** * A class-supporter with an Yggdrasil state */ enum class State { Unknown, Disabled, Enabled, Connected, Reconnecting; } ================================================ FILE: app/src/main/java/eu/neilalexander/yggdrasil/YggTileService.kt ================================================ package eu.neilalexander.yggdrasil import android.content.Intent import android.graphics.drawable.Icon import android.os.Build import android.os.IBinder import android.service.quicksettings.Tile import android.service.quicksettings.TileService import android.util.Log import androidx.annotation.RequiresApi import androidx.core.content.edit import androidx.preference.PreferenceManager private const val TAG = "TileService" @RequiresApi(Build.VERSION_CODES.N) class YggTileService: TileService(), YggStateReceiver.StateReceiver { private lateinit var receiver: YggStateReceiver override fun onCreate() { super.onCreate() receiver = YggStateReceiver(this) } /** * We need to override the method onBind to avoid crashes that were detected on Android 8 * * The possible reason of crashes is described here: * https://github.com/aosp-mirror/platform_frameworks_base/commit/ee68fd889c2dfcd895b8e73fc39d7b97826dc3d8 */ override fun onBind(intent: Intent?): IBinder? { return try { super.onBind(intent) } catch (th: Throwable) { null } } override fun onTileAdded() { super.onTileAdded() updateTileState((application as GlobalApplication).getCurrentState()) } override fun onTileRemoved() { super.onTileRemoved() updateTileState((application as GlobalApplication).getCurrentState()) } override fun onStartListening() { super.onStartListening() receiver.register(this) updateTileState((application as GlobalApplication).getCurrentState()) } override fun onStopListening() { super.onStopListening() receiver.unregister(this) } override fun onDestroy() { super.onDestroy() receiver.unregister(this) } override fun onClick() { super.onClick() // Saving new state val preferences = PreferenceManager.getDefaultSharedPreferences(this.baseContext) val enabled = preferences.getBoolean(PREF_KEY_ENABLED, false) preferences.edit(commit = true) { putBoolean(PREF_KEY_ENABLED, !enabled) } // Starting or stopping VPN service val intent = Intent(this, PacketTunnelProvider::class.java) intent.action = PacketTunnelProvider.ACTION_TOGGLE startService(intent) } private fun updateTileState(state: State) { val tile = qsTile ?: return val oldState = tile.state val preferences = PreferenceManager.getDefaultSharedPreferences(this.baseContext) val enabled = preferences.getBoolean(PREF_KEY_ENABLED, false) tile.state = when (enabled) { false -> Tile.STATE_INACTIVE true -> Tile.STATE_ACTIVE } var changed = oldState != tile.state if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { val oldText = tile.subtitle tile.subtitle = when (state) { State.Enabled -> getText(R.string.tile_enabled) State.Connected -> getText(R.string.tile_connected) else -> getText(R.string.tile_disabled) } changed = changed || (oldText != tile.subtitle) } // Update tile if changed state if (changed) { Log.i(TAG, "Updating tile, old state: $oldState, new state: ${tile.state}") /* Force set the icon in the tile, because there is a problem on icon tint in the Android Oreo. Issue: https://github.com/AdguardTeam/AdguardForAndroid/issues/1996 */ tile.icon = Icon.createWithResource(applicationContext, R.drawable.ic_tile_icon) tile.updateTile() } } override fun onStateChange(state: State) { updateTileState(state) } } ================================================ FILE: app/src/main/res/drawable/ic_baseline_add_circle_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_chevron_right_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_remove_circle_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_tile_icon.xml ================================================ ================================================ FILE: app/src/main/res/drawable/rounded.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_dns.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_main.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_peers.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_settings.xml ================================================ ================================================ FILE: app/src/main/res/layout/dialog_add_dns_server.xml ================================================ ================================================ FILE: app/src/main/res/layout/dialog_addpeer.xml ================================================ ================================================ FILE: app/src/main/res/layout/dialog_resetconfig.xml ================================================ ================================================ FILE: app/src/main/res/layout/dialog_set_keys.xml ================================================ ================================================ FILE: app/src/main/res/layout/dns_server_usable.xml ================================================ ================================================ FILE: app/src/main/res/layout/peers_configured.xml ================================================ ================================================ FILE: app/src/main/res/layout/peers_connected.xml ================================================ ================================================ FILE: app/src/main/res/values/attrs.xml ================================================ ================================================ FILE: app/src/main/res/values/colors.xml ================================================ #FFBB86FC #FF6200EE #FF3700B3 #FF03DAC5 #FF018786 #5FBF9F #FF000000 #FFFFFFFF #F2F1F5 #1C1C1E ================================================ FILE: app/src/main/res/values/strings.xml ================================================ Yggdrasil Copied to clipboard Use these DNS servers while Yggdrasil is running. Note that all DNS requests, including for non-Yggdrasil internet hostnames, will be sent to these servers. Yggdrasil will not configure any DNS servers when the service starts. All DNS requests will be resolved by the default resolver. These DNS servers are provided by community members. Click the + button to add them to the list above. Long-tap to see more info. No servers configured Configured servers The server supports resolving regular ICANN domains, ALFIS domains, OpenNIC domains.\n\nAlso, it blocks ads, analytics and malware websites.\n\nThe server is run by Revertron. Server info OK Cancel Remove Add Add DNS server DNS Usable servers Fix Chrome-based browsers If you do not have IPv6 internet connectivity, this option should help Chrome-based browsers to resolve Yggdrasil domain names correctly. DNS fixes No servers 1 server %d servers Remove %s? This server is already added. Enabled (No connectivity) Connected Not enabled No peers 1 peer %d peers Note No peers are configured. If there are no multicast peers nearby, you will need to manually configure peers in order for Yggdrasil to connect and work properly. Add Configured Peer Add Remove %s? Remove No peers currently configured Configured Peers No peers currently connected Connected Peers Warning Config Reset Status Enable Yggdrasil Network info N/A IP Subnet Configuration Peers DNS servers Settings Version Unknown You must re-enable Yggdrasil after modifying Peers, DNS servers or Settings to make any changes effective. Peer Connectivity Discoverable over multicast Search for multicast peers Yggdrasil will automatically attempt to connect to configured peers when started. If you configure more than one peer, your device may carry traffic on behalf of other network nodes. Avoid this by configuring only a single peer. You can find public peers by opening this link. Multicast peers will be discovered on the same Wi-Fi network or via USB. They must have the same password. Data charges may apply when using mobile data. You can prevent data usage in the device settings. Password Node Info Device Name Tap to edit Information entered here is public and may be shown on network maps. Public Key Your public key forms your identity on the network. It is safe to be shared. Regenerate keys (and IPv6-address) Reset configuration Resetting will overwrite with newly generated configuration. Your public keys and IP address on the network will change. Disabled Enabled (No connectivity) Connected Amsterdam, NL Frankfurt, DE Bratislava, SK Buffalo, US VPN Service Main channel for foreground notification Tap here to enable Yggdrasil. Enter the full URI of the peer to add. Yggdrasil will automatically connect to this peer when started. Invalid URI format Scheme must be one of: %s Host is required Port is required Private key: Set your own key Save No browser found to open the URL! ================================================ FILE: app/src/main/res/values/styles.xml ================================================ ================================================ FILE: app/src/main/res/values/themes.xml ================================================ ================================================ FILE: app/src/main/res/values-night/themes.xml ================================================ ================================================ FILE: app/src/main/res/values-ru/strings.xml ================================================ Yggdrasil Скопировано в буфер Эти серверы DNS будут использоваться при включении Yggdrasil. Заметьте, что все запросы в DNS, даже о доменах не в Yggdrasil, будут отправляться на эти серверы. Yggdrasil не будет переопределять серверы DNS при старте. Все запросы DNS будут разрешаться серверами по умолчанию. Эти серверы DNS предоставляются членами коммюнити. Нажмите кнопку + чтобы добавить их в список выше. Долгое нажатие чтобы посмотреть информацию. Серверы не настроены Настроенные серверы Этот сервер поддерживает работу с обычными доменами ICANN, системой ALFIS, доменами OpenNIC.\n\nКроме того, он блокирует рекламу, системы слежения и зловредные домены.\n\nАдминистратор сервера Revertron. О сервере Ок Отмена Убрать Добавить Добавить сервер DNS DNS Рекомендуемые серверы Обхитрить браузеры на основе Chrome Если у вас нет обычного подключения по IPv6, эта опция должна заставить браузеры на движке Chrome всё равно запрашивать записи IPv6. DNS трюки Нет серверов 1 сервер %d сервера/серверов Убрать %s? Этот сервер уже добавлен. Включено (Нет подключения) Подключено Выключено Нет пиров 1 пир %d пира/пиров Внимание Не настроено ни одного пира. Если не будет обнаруживаемых пиров в этой сети, то вам надо будет добавить пир вручную, чтобы подключение к Yggdrasil работало как положено. Добавить пира в конфиг Добавить Убрать %s? Убрать Пиры не добавлены Добавленные пиры Нет подключенных пиров Подключенные пиры Внимание Конфигурация Сброс Состояние Включить Yggdrasil Адрес и сеть Н/Д Адрес Подсеть Конфигурация Пиры Серверы DNS Настройки Версия Не известно Вы должны перезапустить Yggdrasil после изменения пиров, серверов DNS или настроек, чтобы изменения вступили в силу. Подключения пиров Находимый через multicast Искать пиров через multicast Yggdrasil будет пытаться подключаться к этим пирам автоматически. Если вы добавите несколько пиров, ваше устройство может быть использовано для переноса данных между другими узлами сети. Чтобы этого избежать настройте только один пир. Вы можете найти публичные пиры по этой ссылке. Пиры могут быть найдены с помощью Multicast если они находятся в той же Wi-Fi сети, либо через USB. У них должен быть одинаковый пароль. Трафик в мобильной сети может быть платным. Вы можете отключить мобильные данные в настройках устройства. Пароль Об узле Название устройства Нажмите для изменения Эта информация публична и может появиться на картах сети. Публичный ключ Ваш публичный ключ идентифицирует вас в сети. Его распространение безопасно. Сбросить ключи (и адрес IPv6) Сбросить настройки Сброс создаст полностью новые настройки. Это изменит ваш публичный ключ и адрес IP. Выключено Включено (Нет подключения) Подключено Амстердам, Нидерланды Франкфурт, Германия Братислава, Словакия Буффало, США Сервис VPN Главный канал нотификаций сервиса Нажмите здесь чтобы включить Yggdrasil. Введите полный URI пира для добавления. Yggdrasil будет автоматически подключаться к нему при запуске. Неверный формат URI Протокол должен быть одним из: %s Необходимо указать хост Необходимо указать порт Приватный ключ: Установить свой ключ Сохранить Не найден браузер для открытия ссылки! ================================================ FILE: app/src/test/java/eu/neilalexander/yggdrasil/ExampleUnitTest.kt ================================================ package eu.neilalexander.yggdrasil import org.junit.Test import org.junit.Assert.* /** * Example local unit test, which will execute on the development machine (host). * * See [testing documentation](http://d.android.com/tools/testing). */ class ExampleUnitTest { @Test fun addition_isCorrect() { assertEquals(4, 2 + 2) } } ================================================ FILE: build.gradle ================================================ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { ext.kotlin_version = '1.9.20' repositories { google() mavenCentral() } dependencies { classpath 'com.android.tools.build:gradle:8.1.4' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } } allprojects { repositories { google() mavenCentral() } } task clean(type: Delete) { delete rootProject.buildDir } ================================================ FILE: fastlane/metadata/android/en-US/changelogs/11.txt ================================================ * Added quick-settings tile for fast on/off switching * Added notification with the connection status (it is needed for quick-settings tile) * Fixed small UI bugs * Updated Yggdrasil library to 0.4.7 ================================================ FILE: fastlane/metadata/android/en-US/changelogs/15.txt ================================================ * Updated core Yggdrasil library to 0.5.1 * Updated UI to reflect changes in new version * Fixed small bugs in UI ================================================ FILE: fastlane/metadata/android/en-US/changelogs/16.txt ================================================ * Updated core Yggdrasil library to 0.5.4 ================================================ FILE: fastlane/metadata/android/en-US/changelogs/17.txt ================================================ * Updated core Yggdrasil library to 0.5.6 ================================================ FILE: fastlane/metadata/android/en-US/changelogs/18.txt ================================================ Updated core library to 0.5.7, presenting these changes: Added WebSocket support for peerings, by using the new ws:// scheme in Listen and Peers Additionally, the wss:// scheme can be used to connect to a WebSocket peer behind a HTTPS reverse proxy Changed On Linux, the TUN adapter now uses vectorised reads/writes where possible, which should reduce the amount of CPU time spent on syscalls and potentially improve throughput Link error handling has been improved and various link error messages have been rewritten to be clearer Upgrade dependencies Fixed Multiple multicast connections to the same remote machine should now work correctly You may get two connections in some cases, one inbound and one outbound, this is known and will not cause problems ================================================ FILE: fastlane/metadata/android/en-US/changelogs/19.txt ================================================ Updated core library to 0.5.9, presenting these changes: Changed The routing algorithm has been updated with RTT-aware link costing, which should prefer lower latency links over higher latency links where possible The calculated cost is an average of the link RTT, but newly established links are costed higher to begin with, such that unstable peerings can be avoided Link costs are only used where multiple next-hops are available and will be ignored if there is only one loop-free path to the destination This is protocol-compatible with existing v0.5.x nodes but will have the best results when peering with nodes that are also running the latest version The getPeers endpoint will now report the calculated link cost for each given peer Upgrade dependencies Fixed Multicast discovery should now work again when building Yggdrasil as an Android framework Multicast discovery will now correctly ignore interfaces that are not marked as running Ephemeral links, such as those added by multicast, will no longer try to reconnect in a fast loop, fixing a high CPU issue The TUN interface will no longer stop working when hitting a segment read error from vectorised reads The AllowedPublicKeys option will once again no longer apply to multicast peerings, as was originally intended A potential panic when shutting down peering links has been fixed A redundant system call for setting MTU on OpenBSD has been removed Fixes in Android app Fixed occasional crash on start/stop Updated some dependencies Updated Android API to 34 ================================================ FILE: fastlane/metadata/android/en-US/changelogs/20.txt ================================================ Updated core library to 0.5.12, presenting these changes: Fixed A timing regression which causes a higher level of idle protocol traffic on each peering has been fixed Fixes in Android app Some UI fixes and improvements Updated some dependencies Updates from previous versions: Changed The parent selection algorithm now only chooses a new parent if there is a larger cost benefit to doing so, which should help to stabilise the tree The bloom filters are now repropagated periodically, to avoid nodes getting stuck with bad state Fixed A memory leak caused by missed cleanup of the peer response map has been fixed Other bug fixes with bloom filter propagation for off-tree filters and zero vs one bits TLS-based peering connections now support TLS 1.2 again ================================================ FILE: fastlane/metadata/android/en-US/changelogs/21.txt ================================================ Updated core library to 0.5.13. Added peer validation on input. ================================================ FILE: fastlane/metadata/android/en-US/full_description.txt ================================================ Yggdrasil is an early-stage implementation of a fully end-to-end encrypted IPv6 network. It is lightweight, self-arranging, supported on multiple platforms and allows pretty much any IPv6-capable application to communicate securely with other Yggdrasil nodes. Yggdrasil does not require you to have IPv6 Internet connectivity - it also works over IPv4. This app allows you to connect to Yggdrasil Network and use any service located in this network. It works as VPN service, but all your usual traffic will go trough your provider, not through Yggdrasil Network. Also, it is not a goal of the Yggdrasil project to provide anonymity. Direct peers over the Internet will be able to see your IP address and may be able to use this information to determine your location or identity. Multicast-discovered peerings on the same network will typically expose your device MAC address. Other nodes on the network may be able to discern some information about which nodes you are peered with. All traffic sent across the Yggdrasil network is encrypted end-to-end. Assuming that our crypto is solid, it cannot be decrypted or read by any intermediate nodes, and can only be decrypted by the recipient for which it was intended. However, please note that Yggdrasil has not been officially externally audited. ================================================ FILE: fastlane/metadata/android/en-US/short_description.txt ================================================ Official implementation for connecting to the Yggdrasil Network from Android ================================================ FILE: fastlane/metadata/android/en-US/title.txt ================================================ Yggdrasil ================================================ FILE: fastlane/metadata/android/ru/changelogs/11.txt ================================================ * Добавлена кнопка в шторку для быстрого включения/выключения * Добавлена компактная нотификация со статусом подключения (требуется для работы кнопки в шторке) * Исправлены некоторые баги в интерфейсе * Обновлена библиотека Yggdrasil до 0.4.7 ================================================ FILE: fastlane/metadata/android/ru/changelogs/15.txt ================================================ * Обновлена библиотека Yggdrasil до 0.5.1 * Обновлен интерфейс в соответствии с новой версией библиотеки * Сделаны небольшие исправления в интерфейсе ================================================ FILE: fastlane/metadata/android/ru/changelogs/16.txt ================================================ * Обновлена библиотека Yggdrasil до 0.5.4 ================================================ FILE: fastlane/metadata/android/ru/changelogs/17.txt ================================================ * Обновлена библиотека Yggdrasil до 0.5.6 ================================================ FILE: fastlane/metadata/android/ru/changelogs/18.txt ================================================ Обновлена основная библиотека до версии 0.5.7, в которой представлены следующие изменения: Добавлено Поддержка WebSocket для пиринга с использованием новой схемы ws:// в Listen и Peers Кроме того, схему wss:// можно использовать для подключения к узлам WebSocket за обратным прокси-сервером HTTPS вроде Nginx Изменено В Linux адаптер TUN теперь использует векторизованную чтение/запись, где это возможно, что должно сократить количество времени ЦП, затрачиваемого на системные вызовы, и потенциально повысить пропускную способность Улучшена обработка ошибок соединения, а различные сообщения об ошибках соединения были переписаны для большей ясности Обновление зависимостей Исправлено Несколько мультикаст подключений к одной и той же удаленной машине теперь должны работать правильно В некоторых случаях вы можете получить два подключения, одно входящее и одно исходящее, это известное поведение и не вызовет проблем ================================================ FILE: fastlane/metadata/android/ru/changelogs/19.txt ================================================ Обновлена основная библиотека до версии 0.5.9, в которой представлены следующие изменения: Изменено Алгоритм маршрутизации был обновлен с учетом стоимости соединения с RTT, что должно отдавать предпочтения соединениям с меньшей задержкой соединениям с большей задержкой, когда это возможно Расчетная стоимость представляет собой среднее значение RTT соединения, но новые соединения изначально оцениваются выше, так что можно избежать проблем с нестабильными узлами Стоимость соединения используется только при наличии нескольких следующих переходов и будет игнорироваться, если есть только один путь без петель к месту назначения Эта версия совместима с существующими узлами v0.5.x, но будет иметь наилучшие результаты при пиринге с узлами, которые также работают под управлением последней версии Команда getPeers теперь будет сообщать рассчитанную стоимость соединения для каждого заданного пира Обновлены зависимости Исправлено Обнаружение локальных пиров теперь должно снова работать при сборке Yggdrasil как фреймворка Android Обнаружение локальных пиров теперь будет правильно игнорировать интерфейсы, которые не помечены как работающие Эфемерные соединения, такие как добавленные мультикастом, больше не будут пытаться быстро переподключаться в цикле, устранена проблема высокой загрузки ЦП Интерфейс TUN больше не будет прекращать работу при срабатывании ошибки чтения пакета с помощью векторизованного чтения Опция AllowedPublicKeys снова больше не будет применяться к локальным пирам, как изначально предполагалось Потенциальный краш при отключении пиринговых соединений был исправлен Избыточный системный вызов для установки MTU в OpenBSD был удален Исправления в приложении для Android Исправлен случайный сбой при запуске/остановке Обновлены некоторые зависимости Обновлен Android API до 34 ================================================ FILE: fastlane/metadata/android/ru/changelogs/20.txt ================================================ Обновлена основная библиотека до версии 0.5.12, в которой представлены следующие изменения: Исправлено Исправлена регрессия синхронизации, которая приводит к более высокому уровню служебного трафика в простое Исправления в приложении Android Исправления и улучшения пользовательского интерфейса Обновлены некоторые зависимости Обновления с предыдущих версий: Изменено Алгоритм выбора родителя теперь выбирает нового родителя только в том случае, если это дает большую экономическую выгоду, что должно помочь стабилизировать дерево Фильтры Блума теперь периодически распространяются повторно, чтобы избежать застревания узлов в плохом состоянии Исправлено Утечка памяти, вызванная пропущенной очисткой карты ответов пиров Другие исправления ошибок с распространением фильтра Блума для фильтров вне дерева Пиринг с использованием TLS теперь снова поддерживают TLS 1.2 ================================================ FILE: fastlane/metadata/android/ru/changelogs/21.txt ================================================ Обновлена основная библиотека до версии 0.5.13. Добавлена валидация пира при вводе. ================================================ FILE: fastlane/metadata/android/ru/full_description.txt ================================================ Yggdrasil это рабочая реализация полностью зашифрованной сети IPv6. Она лёгкая, само-организующаяся, поддерживающая множество платформ, и позволяющая любому ПО, способному работать с IPv6, свободно коммуницировать с другими узлами сети Yggdrasil. Yggdrasil не требует Интернет-подключения по IPv6 - она работает и поверх IPv4. Это приложение позволяет вам подключиться к Yggdrasil Network и пользоваться любыми сервисами, расположенными в этой сети. Оно работает как сервис VPN, но весь ваш обычный Интернет-трафик будет идти через вашего провайдера, не через сеть Yggdrasil. Заметьте, что анонимность не является целью Yggdrasil. Прямые пиры через Интернет смогут видеть ваш настоящий адрес IP и смогут использовать эту информацию для определения вашего местоположения или идентификации. Пиры, подключающиеся с помощью multicast, скорее всего смогут узнать ваш MAC-адрес. Некоторые узлы в сети могут вычислить через какие узлы вы входите в сеть Yggdrasil. Весь трафик, пересылаемый через сеть Yggdrasil зашифрован от точки к точке. Предполагая, что наше шифрование реализовано правильно, трафик не может быть расшифрован или прочитан промежуточными узлами, иможет быть расшифрован только адресатом. Однако, просим заметить, что Yggdrasil не подвергался официальному внешнему аудиту. ================================================ FILE: fastlane/metadata/android/ru/short_description.txt ================================================ Официальный клиент для подключения к Yggdrasil Network с устройств Android ================================================ FILE: fastlane/metadata/android/ru/title.txt ================================================ Yggdrasil ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ #Mon Nov 27 01:27:23 CET 2023 distributionBase=GRADLE_USER_HOME distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME ================================================ FILE: gradle.properties ================================================ # Project-wide Gradle settings. # IDE (e.g. Android Studio) users: # Gradle settings configured through the IDE *will override* # any settings specified in this file. # For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true # AndroidX package structure to make it clearer which packages are bundled with the # Android operating system, and which are packaged with your app"s APK # https://developer.android.com/topic/libraries/support-library/androidx-rn android.useAndroidX=true # Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official #android.enableR8.fullMode=false android.defaults.buildfeatures.buildconfig=true android.nonTransitiveRClass=false android.nonFinalResIds=false ================================================ FILE: gradlew ================================================ #!/usr/bin/env sh ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS="" # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn () { echo "$*" } die () { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin, switch paths to Windows format before running java if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=$((i+1)) done case $i in (0) set -- ;; (1) set -- "$args0" ;; (2) set -- "$args0" "$args1" ;; (3) set -- "$args0" "$args1" "$args2" ;; (4) set -- "$args0" "$args1" "$args2" "$args3" ;; (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Escape application args save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } APP_ARGS=$(save "$@") # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then cd "$(dirname "$0")" fi exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS= @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto init echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto init echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :init @rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args :win9xME_args @rem Slurp the command line arguments. set CMD_LINE_ARGS= set _SKIP=2 :win9xME_args_slurp if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: readme.md ================================================ Yggdrasil Android ----------------- Yggdrasil is an early-stage implementation of a fully end-to-end encrypted IPv6 network. It is lightweight, self-arranging, supported on multiple platforms and allows pretty much any IPv6-capable application to communicate securely with other Yggdrasil nodes. Yggdrasil does not require you to have IPv6 Internet connectivity - it also works over IPv4. This app allows you to connect to Yggdrasil Network and use any service located in this network. It works as VPN service, but all your usual traffic will go trough your provider, not through Yggdrasil Network. Also, it is not a goal of the Yggdrasil project to provide anonymity. Direct peers over the Internet will be able to see your IP address and may be able to use this information to determine your location or identity. Multicast-discovered peerings on the same network will typically expose your device MAC address. Other nodes on the network may be able to discern some information about which nodes you are peered with. All traffic sent across the Yggdrasil network is encrypted end-to-end. Assuming that our crypto is solid, it cannot be decrypted or read by any intermediate nodes, and can only be decrypted by the recipient for which it was intended. However, please note that Yggdrasil has not been officially externally audited. ## Download [Get it on F-Droid](https://f-droid.org/packages/eu.neilalexander.yggdrasil/) Or get the APK from the [Releases Section](https://github.com/yggdrasil-network/yggdrasil-android/releases/latest). ## Build Instructions * install gomobile ```bash go install golang.org/x/mobile/cmd/gomobile@latest ``` * clone yggdrasil-android and initialize the yggdrasil-go submodule ```bash git clone https://github.com/yggdrasil-network/yggdrasil-android /tmp/yggdrasil-android cd /tmp/yggdrasil-android git submodule update --init ``` * build yggdrasil-go for android and copy over the built library ```bash cd libs/yggdrasil-go ./contrib/mobile/build -a cp yggdrasil.aar ../../app/libs/ cd ../.. ``` * build yggdrasil-android ```bash ./gradlew assembleRelease ``` ## Updating yggdrasil-go to the Latest Release The yggdrasil-go library is pinned as a git submodule in `libs/yggdrasil-go`. To update it to the latest release and rebuild: ```bash cd libs/yggdrasil-go git fetch --tags git checkout $(git describe --tags $(git rev-list --tags --max-count=1)) ``` Then rebuild the library and copy it: ```bash ./contrib/mobile/build -a cp yggdrasil.aar ../../app/libs/ cd ../.. ``` Finally, commit the submodule update: ```bash git add libs/yggdrasil-go git commit -m "Update yggdrasil-go to $(cd libs/yggdrasil-go && git describe --tags)" ``` note: you will need to use jdk-11 as jdk-16 `"doesn't work" ™` on debian/ubuntu you can set which jdk used with the `JAVA_HOME` env var: ``` export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64/ ``` ================================================ FILE: settings.gradle ================================================ rootProject.name = "Yggdrasil" include ':app'