LockdownVpnTracker.java revision 69ddab4575ff684c533c995e07ca15fe18543fc0
1/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.server.net;
18
19import static android.Manifest.permission.CONNECTIVITY_INTERNAL;
20
21import android.app.Notification;
22import android.app.NotificationManager;
23import android.app.PendingIntent;
24import android.content.BroadcastReceiver;
25import android.content.Context;
26import android.content.Intent;
27import android.content.IntentFilter;
28import android.net.LinkProperties;
29import android.net.NetworkInfo;
30import android.net.NetworkInfo.DetailedState;
31import android.net.NetworkInfo.State;
32import android.os.INetworkManagementService;
33import android.os.RemoteException;
34import android.security.Credentials;
35import android.security.KeyStore;
36import android.text.TextUtils;
37import android.util.Slog;
38
39import com.android.internal.R;
40import com.android.internal.net.VpnConfig;
41import com.android.internal.net.VpnProfile;
42import com.android.internal.util.Preconditions;
43import com.android.server.ConnectivityService;
44import com.android.server.connectivity.Vpn;
45
46/**
47 * State tracker for lockdown mode. Watches for normal {@link NetworkInfo} to be
48 * connected and kicks off VPN connection, managing any required {@code netd}
49 * firewall rules.
50 */
51public class LockdownVpnTracker {
52    private static final String TAG = "LockdownVpnTracker";
53
54    /** Number of VPN attempts before waiting for user intervention. */
55    private static final int MAX_ERROR_COUNT = 4;
56
57    private static final String ACTION_LOCKDOWN_RESET = "com.android.server.action.LOCKDOWN_RESET";
58
59    private final Context mContext;
60    private final INetworkManagementService mNetService;
61    private final ConnectivityService mConnService;
62    private final Vpn mVpn;
63    private final VpnProfile mProfile;
64
65    private final Object mStateLock = new Object();
66
67    private PendingIntent mResetIntent;
68
69    private String mAcceptedEgressIface;
70    private String mAcceptedIface;
71    private String mAcceptedSourceAddr;
72
73    private int mErrorCount;
74
75    public static boolean isEnabled() {
76        return KeyStore.getInstance().contains(Credentials.LOCKDOWN_VPN);
77    }
78
79    public LockdownVpnTracker(Context context, INetworkManagementService netService,
80            ConnectivityService connService, Vpn vpn, VpnProfile profile) {
81        mContext = Preconditions.checkNotNull(context);
82        mNetService = Preconditions.checkNotNull(netService);
83        mConnService = Preconditions.checkNotNull(connService);
84        mVpn = Preconditions.checkNotNull(vpn);
85        mProfile = Preconditions.checkNotNull(profile);
86
87        final Intent intent = new Intent(ACTION_LOCKDOWN_RESET);
88        intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
89        mResetIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
90    }
91
92    private BroadcastReceiver mResetReceiver = new BroadcastReceiver() {
93        @Override
94        public void onReceive(Context context, Intent intent) {
95            reset();
96        }
97    };
98
99    /**
100     * Watch for state changes to both active egress network, kicking off a VPN
101     * connection when ready, or setting firewall rules once VPN is connected.
102     */
103    private void handleStateChangedLocked() {
104        Slog.d(TAG, "handleStateChanged()");
105
106        final NetworkInfo egressInfo = mConnService.getActiveNetworkInfoUnfiltered();
107        final LinkProperties egressProp = mConnService.getActiveLinkProperties();
108
109        final NetworkInfo vpnInfo = mVpn.getNetworkInfo();
110        final VpnConfig vpnConfig = mVpn.getLegacyVpnConfig();
111
112        // Restart VPN when egress network disconnected or changed
113        final boolean egressDisconnected = egressInfo == null
114                || State.DISCONNECTED.equals(egressInfo.getState());
115        final boolean egressChanged = egressProp == null
116                || !TextUtils.equals(mAcceptedEgressIface, egressProp.getInterfaceName());
117        if (egressDisconnected || egressChanged) {
118            clearSourceRules();
119            mAcceptedEgressIface = null;
120            mVpn.stopLegacyVpn();
121        }
122        if (egressDisconnected) return;
123
124        if (mErrorCount > MAX_ERROR_COUNT) {
125            showNotification(R.string.vpn_lockdown_error, R.drawable.vpn_disconnected);
126
127        } else if (egressInfo.isConnected() && !vpnInfo.isConnectedOrConnecting()) {
128            if (mProfile.isValidLockdownProfile()) {
129                Slog.d(TAG, "Active network connected; starting VPN");
130                showNotification(R.string.vpn_lockdown_connecting, R.drawable.vpn_disconnected);
131
132                mAcceptedEgressIface = egressProp.getInterfaceName();
133                mVpn.startLegacyVpn(mProfile, KeyStore.getInstance(), egressProp);
134
135            } else {
136                Slog.e(TAG, "Invalid VPN profile; requires IP-based server and DNS");
137                showNotification(R.string.vpn_lockdown_error, R.drawable.vpn_disconnected);
138            }
139
140        } else if (vpnInfo.isConnected() && vpnConfig != null) {
141            final String iface = vpnConfig.interfaze;
142            final String sourceAddr = vpnConfig.addresses;
143
144            if (TextUtils.equals(iface, mAcceptedIface)
145                    && TextUtils.equals(sourceAddr, mAcceptedSourceAddr)) {
146                return;
147            }
148
149            Slog.d(TAG, "VPN connected using iface=" + iface + ", sourceAddr=" + sourceAddr);
150            showNotification(R.string.vpn_lockdown_connected, R.drawable.vpn_connected);
151
152            try {
153                clearSourceRules();
154
155                mNetService.setFirewallInterfaceRule(iface, true);
156                mNetService.setFirewallEgressSourceRule(sourceAddr, true);
157
158                mErrorCount = 0;
159                mAcceptedIface = iface;
160                mAcceptedSourceAddr = sourceAddr;
161            } catch (RemoteException e) {
162                throw new RuntimeException("Problem setting firewall rules", e);
163            }
164
165            mConnService.sendConnectedBroadcast(augmentNetworkInfo(egressInfo));
166        }
167    }
168
169    public void init() {
170        Slog.d(TAG, "init()");
171
172        mVpn.setEnableNotifications(false);
173
174        final IntentFilter resetFilter = new IntentFilter(ACTION_LOCKDOWN_RESET);
175        mContext.registerReceiver(mResetReceiver, resetFilter, CONNECTIVITY_INTERNAL, null);
176
177        try {
178            // TODO: support non-standard port numbers
179            mNetService.setFirewallEgressDestRule(mProfile.server, 500, true);
180            mNetService.setFirewallEgressDestRule(mProfile.server, 4500, true);
181        } catch (RemoteException e) {
182            throw new RuntimeException("Problem setting firewall rules", e);
183        }
184
185        synchronized (mStateLock) {
186            handleStateChangedLocked();
187        }
188    }
189
190    public void shutdown() {
191        Slog.d(TAG, "shutdown()");
192
193        mAcceptedEgressIface = null;
194        mErrorCount = 0;
195
196        mVpn.stopLegacyVpn();
197        try {
198            mNetService.setFirewallEgressDestRule(mProfile.server, 500, false);
199            mNetService.setFirewallEgressDestRule(mProfile.server, 4500, false);
200        } catch (RemoteException e) {
201            throw new RuntimeException("Problem setting firewall rules", e);
202        }
203        clearSourceRules();
204        hideNotification();
205
206        mContext.unregisterReceiver(mResetReceiver);
207        mVpn.setEnableNotifications(true);
208    }
209
210    public void reset() {
211        // cycle tracker, reset error count, and trigger retry
212        shutdown();
213        init();
214        synchronized (mStateLock) {
215            handleStateChangedLocked();
216        }
217    }
218
219    private void clearSourceRules() {
220        try {
221            if (mAcceptedIface != null) {
222                mNetService.setFirewallInterfaceRule(mAcceptedIface, false);
223                mAcceptedIface = null;
224            }
225            if (mAcceptedSourceAddr != null) {
226                mNetService.setFirewallEgressSourceRule(mAcceptedSourceAddr, false);
227                mAcceptedSourceAddr = null;
228            }
229        } catch (RemoteException e) {
230            throw new RuntimeException("Problem setting firewall rules", e);
231        }
232    }
233
234    public void onNetworkInfoChanged(NetworkInfo info) {
235        synchronized (mStateLock) {
236            handleStateChangedLocked();
237        }
238    }
239
240    public void onVpnStateChanged(NetworkInfo info) {
241        if (info.getDetailedState() == DetailedState.FAILED) {
242            mErrorCount++;
243        }
244        synchronized (mStateLock) {
245            handleStateChangedLocked();
246        }
247    }
248
249    public NetworkInfo augmentNetworkInfo(NetworkInfo info) {
250        final NetworkInfo vpnInfo = mVpn.getNetworkInfo();
251        info = new NetworkInfo(info);
252        info.setDetailedState(vpnInfo.getDetailedState(), vpnInfo.getReason(), null);
253        return info;
254    }
255
256    private void showNotification(int titleRes, int iconRes) {
257        final Notification.Builder builder = new Notification.Builder(mContext);
258        builder.setWhen(0);
259        builder.setSmallIcon(iconRes);
260        builder.setContentTitle(mContext.getString(titleRes));
261        builder.setContentText(mContext.getString(R.string.vpn_lockdown_reset));
262        builder.setContentIntent(mResetIntent);
263        builder.setPriority(Notification.PRIORITY_LOW);
264        builder.setOngoing(true);
265        NotificationManager.from(mContext).notify(TAG, 0, builder.build());
266    }
267
268    private void hideNotification() {
269        NotificationManager.from(mContext).cancel(TAG, 0);
270    }
271}
272