Commit b9a9c7da authored by Daniel Wolf's avatar Daniel Wolf
Browse files
parents 8f38329a 36b24ab9
# FAQ
A selection of common questions and a collection of technical aspects of Nebulo. Feel free to ask a question in the issues and I'll add it here as well.
## Non-VPN mode
Since 1.4.0 Nebulo can run without requiring the dummy VPN. In this mode Nebulo hosts a DNS server locally, which forwards all DNS queries it receives according to the settings you configured in Nebulo.<br>
In this mode you manually have to forward all the DNS queries your decice creates to Nebulos local DNS server (normally this is what the dummy VPN is used for).<br>
If your device is rooted Nebulo has an inbuilt solution using `iptables`. If it isn't rooted you have to use third-party apps which are able to forward the DNS queries to Nebulo.
Known third-party apps this works with are NetGuard and V2Ray (although there might be others). You can find instructions on how to configure these apps to work together with Nebulo in the settings.<br>
Please note that the App exclusion setting inside the general category won't have any effect in non-VPN mode. You have to configure excluded apps inside the third-party app you are using.
## Query logging
### What do the icons mean in the query log?
There are 4 icons in the query log:
- Database/server icon: cache is enabled and the DNS response came from cache
- Flag: The response came from the DNS rules OR the upstream DNS server replied with 0.0.0.0 or ::1
- Left pointing arrow: The answer was forwarded to a DNS server
- Questionmark: Unknown what happened with the query (normally you shouldn't see that)
## ESNI
### Is ESNI supported?
Currently no. The Android platform and the libraries I am using lack support for ESNI (https://git.frostnerd.com/PublicAndroidApps/smokescreen/-/issues/237)
### Do I need ESNI?
Most likely no. It would make it harder for government/ISPs to block access to a DNS server though.
\ No newline at end of file
......@@ -30,12 +30,7 @@ The second topic, tracking, is nearly as important as the topic of censorship. M
Nebulo uses the VPN API of the Android system to create a dummy VPN which intercepts all packets for the dns servers of your device. This dummy VPN is __not__ a real VPN and does not tunnel your packets - it only handles dns packets. As only one VPN can be activate at any given time you have to decice between using Nebulo or a real VPN.
## Non-VPN mode
Since 1.4.0 Nebulo can run without requiring the dummy VPN. In this mode Nebulo hosts a DNS server locally, which forwards all DNS queries it receives according to the settings you configured in Nebulo.<br>
In this mode you manually have to forward all the DNS queries your decice creates to Nebulos local DNS server (normally this is what the dummy VPN is used for).<br>
If your device is rooted Nebulo has an inbuilt solution using `iptables`. If it isn't rooted you have to use third-party apps which are able to forward the DNS queries to Nebulo.
Known third-party apps this works with are NetGuard and V2Ray (although there might be others). You can find instructions on how to configure these apps to work together with Nebulo in the settings.<br>
Please note that the App exclusion setting inside the general category won't have any effect in non-VPN mode. You have to configure excluded apps inside the third-party app you are using.
Look in the [FAQ](FAQ.md).
## What this is based on
Nebulo is a completely original piece of software. It doesn't use any other dependency under the hood for the dns capabilities. Check the [dependencies](https://git.frostnerd.com/PublicAndroidApps/smokescreen/blob/master/app/build.gradle#L100) to see what is used for everything build around DoH/DoT.
......@@ -65,6 +60,9 @@ The app consists of a few core features:
* Allow search domains on the current network
* ... And more
# FAQ
For a growing collection of frequenty asked questions (FAQ), take a look [here](FAQ.md).
# Help wanted
Requesting your support: the app is getting closer to a proper release but it's still missing an important aspect: translations.
Translations are important to reach as broad of an audience as possible and for non-english speakers to be able to use the app to it's full extent.
......
......@@ -48,6 +48,13 @@ fun showPrivacyPolicyDialog(context: Context) {
dialog.show()
}
fun showInfoTextDialogWithClose(
context: Context,
title: String,
text: String,
withDialog: (AlertDialog.() -> Unit)? = null,
) = showInfoTextDialog(context, title, text, neutralButton = null, positiveButton = context.getString(R.string.all_close) to null, withDialog = withDialog)
fun showInfoTextDialog(context:Context,
title:String,
text:String,
......
......@@ -125,6 +125,11 @@ class MainActivity : NavigationDrawerActivity() {
update()
}
}
view.infoButton.setOnClickListener {
showInfoTextDialogWithClose(this,
getString(R.string.dialog_latency_sidebar_title),
getString(R.string.dialog_latency_sidebar_message))
}
networkManager.registerNetworkCallback(NetworkRequest.Builder().apply {
addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
}.build(), cardNetworkCallback!!)
......
package com.frostnerd.smokescreen.dialog
import android.content.Context
import android.text.Editable
import android.text.TextWatcher
import android.view.View
import androidx.appcompat.app.AlertDialog
import com.frostnerd.smokescreen.R
import com.frostnerd.smokescreen.database.entities.DnsRule
import com.frostnerd.smokescreen.database.entities.HostSource
import com.frostnerd.smokescreen.database.getDatabase
import com.frostnerd.smokescreen.getPreferences
import kotlinx.android.synthetic.main.dialog_dnsrule_search.view.*
import kotlinx.coroutines.*
import org.minidns.record.Record
import kotlin.coroutines.CoroutineContext
/*
* Copyright (C) 2020 Daniel Wolf (Ch4t4r)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* You can contact the developer at daniel.wolf@frostnerd.com.
*/
class DnsRuleSearchDialog(
context: Context
):AlertDialog(context, context.getPreferences().theme.dialogStyle), CoroutineScope {
val supervisor = SupervisorJob()
override val coroutineContext: CoroutineContext = supervisor + Dispatchers.IO
var currentSearchJob:Job? = null
private val watcher = object :TextWatcher{
private var previousSearch = ""
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
val search = s.toString().trim()
if (search == previousSearch)
return
previousSearch = search
currentSearchJob?.cancel()
if(search.isNotBlank()) currentSearchJob = this@DnsRuleSearchDialog.launch {
delay(500) // debounce
if (search != previousSearch)
return@launch
val rule = findRuleForSearch(search)
val hostSource = rule?.importedFrom?.let { getHostSourceForId(it) }
if(isActive) launch(Dispatchers.Main) {
if(rule == null) displayRuleSource(wasFound = false, isUserRule = false, null)
else displayRuleSource(wasFound = true, isUserRule = rule.importedFrom == null, hostSource)
}
}
else clearSearchResultText()
}
override fun afterTextChanged(s: Editable?) = Unit
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
}
val view: View
init {
view = layoutInflater.inflate(R.layout.dialog_dnsrule_search, null, false)
setTitle(R.string.dialog_dnsrules_search_title)
setView(view)
setButton(BUTTON_POSITIVE, context.getText(R.string.all_close)) {_,_ ->
supervisor.cancel()
}
view.searchTerm.addTextChangedListener(watcher)
setOnDismissListener {
supervisor.cancel()
}
}
private fun findRuleForSearch(searchTerm: String): DnsRule? {
var source: DnsRule? = null
if (searchTerm.startsWith("www", ignoreCase = true)) {
source = context.getDatabase().dnsRuleDao().findRuleTargetEntity(
searchTerm.replaceFirst("www.", "", ignoreCase = true),
Record.TYPE.ANY,
true
)
}
source = source ?: context.getDatabase().dnsRuleDao()
.findRuleTargetEntity(searchTerm, Record.TYPE.ANY, true)
return source ?: context.getDatabase().dnsRuleDao().findPossibleWildcardRuleTarget(
searchTerm, type = Record.TYPE.ANY,
useUserRules = true,
includeWhitelistEntries = false,
includeNonWhitelistEntries = true
).firstOrNull {
DnsRuleDialog.databaseHostToMatcher(it.host).reset(searchTerm).matches()
}
}
private fun getHostSourceForId(id:Long):HostSource? {
return context.getDatabase().hostSourceDao().findById(id)
}
private fun displayRuleSource(wasFound:Boolean, isUserRule:Boolean, hostSource: HostSource?) {
val text = if(wasFound) {
if(isUserRule) {
context.getString(R.string.dialog_dnsrules_status_userrule)
} else {
if(hostSource == null) {
context.getString(R.string.dialog_dnsrules_status_sourcenotfound)
} else {
context.getString(R.string.dialog_dnsrules_status_fromsource, hostSource.name)
}
}
} else {
context.getString(R.string.dialog_dnsrules_status_not_found)
}
view.searchResult.text = text
}
private fun clearSearchResultText() {
view.searchResult.text = ""
}
}
\ No newline at end of file
package com.frostnerd.smokescreen.fragment
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
import android.os.Build
......@@ -7,6 +8,7 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment
import com.frostnerd.smokescreen.*
import com.frostnerd.smokescreen.database.AppDatabase
......@@ -115,5 +117,18 @@ class AboutFragment : Fragment() {
if(queryGenStepOne) QueryGeneratorDialog(requireContext())
true
}
view.faq.setOnClickListener {
try {
startActivity(
Intent(
Intent.ACTION_VIEW,
Uri.parse("https://nebulo.app/faq")
)
)
} catch (e: ActivityNotFoundException) {
Toast.makeText(requireContext(), R.string.error_no_webbrowser_installed, Toast.LENGTH_LONG)
.show()
}
}
}
}
\ No newline at end of file
......@@ -533,7 +533,7 @@ class DnsRuleFragment : Fragment() {
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_dnsrule, menu)
val switch = menu.getItem(0)?.actionView?.findViewById<Switch>(R.id.actionbarSwitch)
val switch = menu.findItem(R.id.rulesEnabled)?.actionView?.findViewById<Switch>(R.id.actionbarSwitch)
switch?.isChecked = getPreferences().dnsRulesEnabled.also {
overlay.visibility = if(it) View.GONE else View.VISIBLE
}
......@@ -541,6 +541,10 @@ class DnsRuleFragment : Fragment() {
getPreferences().dnsRulesEnabled = isChecked
overlay.visibility = if(isChecked) View.GONE else View.VISIBLE
}
menu.findItem(R.id.search).setOnMenuItemClickListener {
DnsRuleSearchDialog(requireContext()).show()
true
}
}
private class SourceViewHolder(
......
......@@ -230,6 +230,7 @@ class MainFragment : Fragment() {
private fun updateVpnIndicators() {
val privateDnsActive = requireContext().isPrivateDnsActive
var startButtonVisibility = View.VISIBLE
var privacyTextVisibility = View.VISIBLE
when(proxyState) {
ProxyState.RUNNING -> {
privateDnsInfo.visibility = View.INVISIBLE
......@@ -252,6 +253,7 @@ class MainFragment : Fragment() {
if (privateDnsActive) {
statusImage.setImageResource(R.drawable.ic_lock)
statusImage.clearAnimation()
privacyTextVisibility = View.INVISIBLE
startButtonVisibility = View.INVISIBLE
privateDnsInfo.visibility = View.VISIBLE
} else {
......@@ -262,6 +264,7 @@ class MainFragment : Fragment() {
}
}
startButton.visibility = startButtonVisibility
privacyTextWrap.visibility = privacyTextVisibility
}
private fun updatePrivacyPolicyLink(serverInfo: DnsServerInformation<*>) {
......
......@@ -12,6 +12,7 @@ import com.frostnerd.smokescreen.R
import com.frostnerd.smokescreen.database.entities.DnsQuery
import com.frostnerd.smokescreen.fragment.querylogfragment.QueryLogDetailFragment
import com.frostnerd.smokescreen.fragment.querylogfragment.QueryLogListFragment
import com.frostnerd.smokescreen.showInfoTextDialogWithClose
import kotlinx.android.synthetic.main.fragment_querylog_main.*
/*
......@@ -68,6 +69,14 @@ class QueryLogFragment : Fragment(), BackpressFragment {
searchView.setSearchableInfo(searchManager.getSearchableInfo(requireActivity().componentName))
searchView.queryHint = getString(R.string.windows_querylogging_search_hint)
searchView.setOnQueryTextListener(listFragment)
menu.findItem(R.id.info)!!.setOnMenuItemClickListener {
showInfoTextDialogWithClose(
requireContext(),
getString(R.string.dialog_querylog_information_title),
getString(R.string.dialog_querylog_information_message)
)
true
}
}
fun displayQueryDetailed(query:DnsQuery, switchToDetailView:Boolean = true) {
......
......@@ -173,7 +173,7 @@ class QueryLogDetailFragment : Fragment() {
private fun showRuleSource(query:DnsQuery) {
hostSourceFetchJob = GlobalScope.launch(Dispatchers.IO) {
val sourceRule = if(query.name.startsWith("www", ignoreCase = true)) {
getDatabase().dnsRuleDao().findRuleTargetEntity(query.name.replaceFirst("www.", ""), query.type, true)
getDatabase().dnsRuleDao().findRuleTargetEntity(query.name.replaceFirst("www.", "", ignoreCase = true), query.type, true)
?: getDatabase().dnsRuleDao().findRuleTargetEntity(query.name, query.type, true)
} else {
getDatabase().dnsRuleDao().findRuleTargetEntity(query.name, query.type, true)
......
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="?attr/navDrawableColor"
android:pathData="M11,17h2v-6h-2v6zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM11,9h2L13,7h-2v2z" />
</vector>
......@@ -16,10 +16,20 @@
android:theme="@style/AppTheme_Mono"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>
<TextView
android:layout_width="wrap_content"
android:layout_centerHorizontal="true"
android:id="@+id/information"
android:layout_marginTop="6dp"
android:layout_below="@id/toolBar"
android:gravity="center_horizontal"
android:text="@string/window_speedtest_information"
android:layout_height="wrap_content" />
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_above="@id/startTest"
android:layout_below="@id/toolBar"
android:layout_below="@id/information"
android:layout_marginTop="12dp"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
......
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:padding="16dp"
android:gravity="center_horizontal"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:text="@string/dialog_dnsrules_search_message"
android:layout_height="wrap_content" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:paddingLeft="32dp"
android:paddingRight="32dp"
android:layout_marginTop="32dp"
android:id="@+id/searchTermTIL"
android:visibility="visible"
app:errorEnabled="true"
android:textColorHint="?android:attr/textColorSecondary"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:hint="@string/hint_searchterm"
android:id="@+id/searchTerm"
android:text="example.com"
tools:ignore="HardcodedText"
android:layout_height="wrap_content"/>
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:layout_width="wrap_content"
android:id="@+id/searchResult"
android:layout_height="wrap_content" />
</LinearLayout>
\ No newline at end of file
......@@ -195,6 +195,28 @@
android:layout_height="wrap_content"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:id="@+id/faq"
android:orientation="horizontal"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:layout_height="wrap_content">
<ImageView
android:layout_width="30dp"
android:src="@drawable/ic_query_question"
android:layout_height="30dp"/>
<TextView
android:layout_width="match_parent"
android:text="@string/about_help_faq"
android:textSize="19sp"
android:layout_marginStart="16dp"
android:layout_gravity="center_vertical"
android:layout_height="wrap_content"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:id="@+id/about"
......
......@@ -40,26 +40,33 @@
android:layout_alignParentBottom="true"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:contentDescription="@string/contentdescription_main_infoimage"
android:drawableStart="@drawable/ic_info"
android:drawablePadding="8dp"
android:text="@string/main_dnssurveillance_infotext" />
<LinearLayout
android:layout_width="match_parent"
android:orientation="vertical"
android:id="@+id/privacyTextWrap"
android:layout_height="wrap_content">
<TextView
android:id="@+id/privacyStatementText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginBottom="8dp"
android:visibility="gone"
android:drawableStart="@drawable/ic_caret_right"
android:drawablePadding="8dp"
android:gravity="center"
android:textStyle="italic" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:contentDescription="@string/contentdescription_main_infoimage"
android:drawableStart="@drawable/ic_info"
android:drawablePadding="8dp"
android:text="@string/main_dnssurveillance_infotext" />
<TextView
android:id="@+id/privacyStatementText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginBottom="8dp"
android:visibility="gone"
android:drawableStart="@drawable/ic_caret_right"
android:drawablePadding="8dp"
android:gravity="center"
android:textStyle="italic" />
</LinearLayout>
<RelativeLayout
android:layout_width="match_parent"
......
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="horizontal"
android:layout_width="match_parent"
android:padding="8dp"
android:layout_height="wrap_content">
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp">
<ImageView
android:id="@+id/icon"
android:layout_width="42dp"
android:layout_height="42dp"
android:layout_centerVertical="true"
android:layout_gravity="center_vertical"
android:src="@drawable/ic_launcher_flat"
android:layout_height="42dp"/>
android:src="@drawable/ic_launcher_flat" />
<ImageButton
android:id="@+id/infoButton"
style="?android:attr/borderlessButtonStyle"
android:layout_width="42dp"
android:layout_height="42dp"
android:layout_alignParentEnd="true"
android:layout_gravity="center_vertical"
android:layout_marginStart="8dp"
android:layout_marginEnd="4dp"
android:adjustViewBounds="true"
android:padding="8dp"
android:scaleType="centerInside"
android:src="@drawable/ic_info" />
<LinearLayout
android:layout_width="match_parent"
android:orientation="vertical"
android:layout_marginLeft="12dp"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_toStartOf="@id/infoButton"
android:layout_toEndOf="@id/icon"
android:orientation="vertical">
<TextView
android:id="@+id/serverName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textStyle="bold"
android:id="@+id/serverName"
android:layout_height="wrap_content"/>
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:id="@+id/latency"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="- ms"
tools:ignore="HardcodedText"
android:layout_height="wrap_content"/>
tools:ignore="HardcodedText" />
<TextView
android:layout_width="wrap_content"
android:id="@+id/dns1"
android:layout_height="wrap_content"/>
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:layout_width="wrap_content"
android:id="@+id/dns2"
android:layout_height="wrap_content"/>
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</LinearLayout>
\ No newline at end of file
</RelativeLayout>
\ No newline at end of file
......@@ -8,4 +8,10 @@
app:showAsAction="always"
app:actionLayout="@layout/actionbar_switch" />
<item
android:id="@+id/search"
android:icon="@drawable/ic_search"
android:title="@string/hint_search"
app:showAsAction="ifRoom" />
</menu>
\ No newline at end of file
......@@ -8,4 +8,10 @@
app:actionViewClass="androidx.appcompat.widget.SearchView"
app:showAsAction="collapseActionView|ifRoom" />
<item
android:id="@+id/info"
android:icon="@drawable/ic_info_24dp"
android:title="@string/window_querylog_information"