Skip to content

Commit

Permalink
Add toggle for auto-reconnection on VPN termination
Browse files Browse the repository at this point in the history
This allows the user to run other VPN apps, disconnecting PCAPdroid, and
restart it when they terminate.

Closes #411
  • Loading branch information
emanuele-f committed Feb 18, 2024
1 parent 85ded37 commit 630e911
Show file tree
Hide file tree
Showing 10 changed files with 353 additions and 28 deletions.
8 changes: 8 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,14 @@
<action android:name="android.net.VpnService" />
</intent-filter>
</service>
<service
android:name=".VpnReconnectService"
android:foregroundServiceType="specialUse"
android:exported="false">

<property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="waits for the termination of the active VpnService"/>
</service>

<receiver android:name="com.emanuelef.remote_capture.ActionReceiver" />

Expand Down
56 changes: 34 additions & 22 deletions app/src/main/java/com/emanuelef/remote_capture/CaptureHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.net.VpnService;
import android.os.Handler;
Expand All @@ -33,6 +34,7 @@
import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;

Expand All @@ -41,33 +43,40 @@

public class CaptureHelper {
private static final String TAG = "CaptureHelper";
private final ComponentActivity mActivity;
private final ActivityResultLauncher<Intent> mLauncher;
private final Context mContext;
private final @Nullable ActivityResultLauncher<Intent> mLauncher;
private final boolean mResolveHosts;
private CaptureSettings mSettings;
private CaptureStartListener mListener;

public CaptureHelper(ComponentActivity activity, boolean resolve_hosts) {
mActivity = activity;
mContext = activity;
mResolveHosts = resolve_hosts;
mLauncher = activity.registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(), this::captureServiceResult);
}

/** Note: This constructor does not handle the first-time VPN prepare */
public CaptureHelper(Context context) {
mContext = context;
mResolveHosts = true;
mLauncher = null;
}

private void captureServiceResult(final ActivityResult result) {
if(result.getResultCode() == Activity.RESULT_OK)
resolveHosts();
else if(mListener != null) {
Utils.showToastLong(mActivity, R.string.vpn_setup_failed);
Utils.showToastLong(mContext, R.string.vpn_setup_failed);
mListener.onCaptureStartResult(false);
}
}

private void startCaptureOk() {
final Intent intent = new Intent(mActivity, CaptureService.class);
final Intent intent = new Intent(mContext, CaptureService.class);
intent.putExtra("settings", mSettings);

ContextCompat.startForegroundService(mActivity, intent);
ContextCompat.startForegroundService(mContext, intent);
if(mListener != null)
mListener.onCaptureStartResult(true);
}
Expand Down Expand Up @@ -121,7 +130,7 @@ private void resolveHosts() {
if(failed_host == null)
startCaptureOk();
else {
Utils.showToastLong(mActivity, R.string.host_resolution_failed, failed_host);
Utils.showToastLong(mContext, R.string.host_resolution_failed, failed_host);
mListener.onCaptureStartResult(false);
}
});
Expand All @@ -139,23 +148,26 @@ public void startCapture(CaptureSettings settings) {
return;
}

Intent vpnPrepareIntent = VpnService.prepare(mActivity);
Intent vpnPrepareIntent = VpnService.prepare(mContext);
if(vpnPrepareIntent != null) {
new AlertDialog.Builder(mActivity)
.setMessage(R.string.vpn_setup_msg)
.setPositiveButton(R.string.ok, (dialog, whichButton) -> {
try {
mLauncher.launch(vpnPrepareIntent);
} catch (ActivityNotFoundException e) {
Utils.showToastLong(mActivity, R.string.no_intent_handler_found);
if (mLauncher != null)
new AlertDialog.Builder(mContext)
.setMessage(R.string.vpn_setup_msg)
.setPositiveButton(R.string.ok, (dialog, whichButton) -> {
try {
mLauncher.launch(vpnPrepareIntent);
} catch (ActivityNotFoundException e) {
Utils.showToastLong(mContext, R.string.no_intent_handler_found);
mListener.onCaptureStartResult(false);
}
})
.setOnCancelListener(dialog -> {
Utils.showToastLong(mContext, R.string.vpn_setup_failed);
mListener.onCaptureStartResult(false);
}
})
.setOnCancelListener(dialog -> {
Utils.showToastLong(mActivity, R.string.vpn_setup_failed);
mListener.onCaptureStartResult(false);
})
.show();
})
.show();
else if (mListener != null)
mListener.onCaptureStartResult(false);
} else
resolveHosts();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with PCAPdroid. If not, see <http://www.gnu.org/licenses/>.
*
* Copyright 2020-21 - Emanuele Faranda
* Copyright 2020-24 - Emanuele Faranda
*/

package com.emanuelef.remote_capture;
Expand Down Expand Up @@ -112,6 +112,7 @@ public class CaptureService extends VpnService implements Runnable {
final Condition mCaptureStopped = mLock.newCondition();
private ParcelFileDescriptor mParcelFileDescriptor;
private boolean mIsAlwaysOnVPN;
private boolean mRevoked;
private SharedPreferences mPrefs;
private CaptureSettings mSettings;
private Billing mBilling;
Expand Down Expand Up @@ -236,6 +237,9 @@ public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
return abortStart();
}

if (VpnReconnectService.isAvailable())
VpnReconnectService.stopService();

mHandler = new Handler(Looper.getMainLooper());
mBilling = Billing.newInstance(this);

Expand Down Expand Up @@ -605,6 +609,7 @@ public void onReceive(Context context, Intent intent) {
@Override
public void onRevoke() {
Log.d(CaptureService.TAG, "onRevoke");
mRevoked = true;
stopService();
super.onRevoke();
}
Expand Down Expand Up @@ -1404,6 +1409,15 @@ private void updateServiceStatus(ServiceStatus cur_status) {
reloadDecryptionList();
reloadBlocklist();
reloadFirewallWhitelist();
} else if (cur_status == ServiceStatus.STOPPED) {
if (mRevoked && Prefs.restartOnDisconnect(mPrefs) && !mIsAlwaysOnVPN && (isVpnCapture() == 1)) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
Log.i(TAG, "VPN disconnected, starting reconnect service");

final Intent intent = new Intent(this, VpnReconnectService.class);
ContextCompat.startForegroundService(this, intent);
}
}
}
}

Expand Down
6 changes: 3 additions & 3 deletions app/src/main/java/com/emanuelef/remote_capture/Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -646,7 +646,7 @@ public Integer next() {
// Using the deprecated API instead to keep things simple.
// https://developer.android.com/reference/android/net/ConnectivityManager#getAllNetworks()
@SuppressWarnings("deprecation")
public static boolean hasVPNRunning(Context context) {
public static Network getRunningVpn(Context context) {
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
if(cm != null) {
try {
Expand All @@ -657,7 +657,7 @@ public static boolean hasVPNRunning(Context context) {

if ((cap != null) && cap.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) {
Log.d("hasVPNRunning", "detected VPN connection: " + net.toString());
return true;
return net;
}
}
} catch (SecurityException e) {
Expand All @@ -666,7 +666,7 @@ public static boolean hasVPNRunning(Context context) {
}
}

return false;
return null;
}

public static void showToast(Context context, int id, Object... args) {
Expand Down
Loading

0 comments on commit 630e911

Please sign in to comment.