LockdownVpnTracker.java revision 0cb7903ddedbbb8a8171926e4460b74af589369d
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.ConnectivityManager;
29import android.net.LinkProperties;
30import android.net.LinkAddress;
31import android.net.NetworkInfo;
32import android.net.NetworkInfo.DetailedState;
33import android.net.NetworkInfo.State;
34import android.os.INetworkManagementService;
35import android.os.RemoteException;
36import android.security.Credentials;
37import android.security.KeyStore;
38import android.text.TextUtils;
39import android.util.Slog;
40
41import com.android.internal.R;
42import com.android.internal.net.VpnConfig;
43import com.android.internal.net.VpnProfile;
44import com.android.internal.util.Preconditions;
45import com.android.server.ConnectivityService;
46import com.android.server.EventLogTags;
47import com.android.server.connectivity.Vpn;
48
49import java.util.List;
50
51/**
52 * State tracker for lockdown mode. Watches for normal {@link NetworkInfo} to be
53 * connected and kicks off VPN connection, managing any required {@code netd}
54 * firewall rules.
55 */
56public class LockdownVpnTracker {
57    private static final String TAG = "LockdownVpnTracker";
58
59    /** Number of VPN attempts before waiting for user intervention. */
60    private static final int MAX_ERROR_COUNT = 4;
61
62    private static final String ACTION_LOCKDOWN_RESET = "com.android.server.action.LOCKDOWN_RESET";
63
64    private static final String ACTION_VPN_SETTINGS = "android.net.vpn.SETTINGS";
65    private static final String EXTRA_PICK_LOCKDOWN = "android.net.vpn.PICK_LOCKDOWN";
66
67    private final Context mContext;
68    private final INetworkManagementService mNetService;
69    private final ConnectivityService mConnService;
70    private final Vpn mVpn;
71    private final VpnProfile mProfile;
72
73    private final Object mStateLock = new Object();
74
75    private final PendingIntent mConfigIntent;
76    private final PendingIntent mResetIntent;
77
78    private String mAcceptedEgressIface;
79    private String mAcceptedIface;
80    private List<LinkAddress> mAcceptedSourceAddr;
81
82    private int mErrorCount;
83
84    public static boolean isEnabled() {
85        return KeyStore.getInstance().contains(Credentials.LOCKDOWN_VPN);
86    }
87
88    public LockdownVpnTracker(Context context, INetworkManagementService netService,
89            ConnectivityService connService, Vpn vpn, VpnProfile profile) {
90        mContext = Preconditions.checkNotNull(context);
91        mNetService = Preconditions.checkNotNull(netService);
92        mConnService = Preconditions.checkNotNull(connService);
93        mVpn = Preconditions.checkNotNull(vpn);
94        mProfile = Preconditions.checkNotNull(profile);
95
96        final Intent configIntent = new Intent(ACTION_VPN_SETTINGS);
97        configIntent.putExtra(EXTRA_PICK_LOCKDOWN, true);
98        mConfigIntent = PendingIntent.getActivity(mContext, 0, configIntent, 0);
99
100        final Intent resetIntent = new Intent(ACTION_LOCKDOWN_RESET);
101        resetIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
102        mResetIntent = PendingIntent.getBroadcast(mContext, 0, resetIntent, 0);
103    }
104
105    private BroadcastReceiver mResetReceiver = new BroadcastReceiver() {
106        @Override
107        public void onReceive(Context context, Intent intent) {
108            reset();
109        }
110    };
111
112    /**
113     * Watch for state changes to both active egress network, kicking off a VPN
114     * connection when ready, or setting firewall rules once VPN is connected.
115     */
116    private void handleStateChangedLocked() {
117
118        final NetworkInfo egressInfo = mConnService.getActiveNetworkInfoUnfiltered();
119        final LinkProperties egressProp = mConnService.getActiveLinkProperties();
120
121        final NetworkInfo vpnInfo = mVpn.getNetworkInfo();
122        final VpnConfig vpnConfig = mVpn.getLegacyVpnConfig();
123
124        // Restart VPN when egress network disconnected or changed
125        final boolean egressDisconnected = egressInfo == null
126                || State.DISCONNECTED.equals(egressInfo.getState());
127        final boolean egressChanged = egressProp == null
128                || !TextUtils.equals(mAcceptedEgressIface, egressProp.getInterfaceName());
129
130        final String egressTypeName = (egressInfo == null) ?
131                null : ConnectivityManager.getNetworkTypeName(egressInfo.getType());
132        final String egressIface = (egressProp == null) ?
133                null : egressProp.getInterfaceName();
134        Slog.d(TAG, "handleStateChanged: egress=" + egressTypeName +
135                " " + mAcceptedEgressIface + "->" + egressIface);
136
137        if (egressDisconnected || egressChanged) {
138            clearSourceRulesLocked();
139            mAcceptedEgressIface = null;
140            mVpn.stopLegacyVpn();
141        }
142        if (egressDisconnected) {
143            hideNotification();
144            return;
145        }
146
147        final int egressType = egressInfo.getType();
148        if (vpnInfo.getDetailedState() == DetailedState.FAILED) {
149            EventLogTags.writeLockdownVpnError(egressType);
150        }
151
152        if (mErrorCount > MAX_ERROR_COUNT) {
153            showNotification(R.string.vpn_lockdown_error, R.drawable.vpn_disconnected);
154
155        } else if (egressInfo.isConnected() && !vpnInfo.isConnectedOrConnecting()) {
156            if (mProfile.isValidLockdownProfile()) {
157                Slog.d(TAG, "Active network connected; starting VPN");
158                EventLogTags.writeLockdownVpnConnecting(egressType);
159                showNotification(R.string.vpn_lockdown_connecting, R.drawable.vpn_disconnected);
160
161                mAcceptedEgressIface = egressProp.getInterfaceName();
162                try {
163                    mVpn.startLegacyVpn(mProfile, KeyStore.getInstance(), egressProp);
164                } catch (IllegalStateException e) {
165                    mAcceptedEgressIface = null;
166                    Slog.e(TAG, "Failed to start VPN", e);
167                    showNotification(R.string.vpn_lockdown_error, R.drawable.vpn_disconnected);
168                }
169            } else {
170                Slog.e(TAG, "Invalid VPN profile; requires IP-based server and DNS");
171                showNotification(R.string.vpn_lockdown_error, R.drawable.vpn_disconnected);
172            }
173
174        } else if (vpnInfo.isConnected() && vpnConfig != null) {
175            final String iface = vpnConfig.interfaze;
176            final List<LinkAddress> sourceAddrs = vpnConfig.addresses;
177
178            if (TextUtils.equals(iface, mAcceptedIface)
179                  && sourceAddrs.equals(mAcceptedSourceAddr)) {
180                return;
181            }
182
183            Slog.d(TAG, "VPN connected using iface=" + iface +
184                    ", sourceAddr=" + sourceAddrs.toString());
185            EventLogTags.writeLockdownVpnConnected(egressType);
186            showNotification(R.string.vpn_lockdown_connected, R.drawable.vpn_connected);
187
188            try {
189                clearSourceRulesLocked();
190
191                mNetService.setFirewallInterfaceRule(iface, true);
192                for (LinkAddress addr : sourceAddrs) {
193                    mNetService.setFirewallEgressSourceRule(addr.toString(), true);
194                }
195
196                mErrorCount = 0;
197                mAcceptedIface = iface;
198                mAcceptedSourceAddr = sourceAddrs;
199            } catch (RemoteException e) {
200                throw new RuntimeException("Problem setting firewall rules", e);
201            }
202
203            mConnService.sendConnectedBroadcast(augmentNetworkInfo(egressInfo));
204        }
205    }
206
207    public void init() {
208        synchronized (mStateLock) {
209            initLocked();
210        }
211    }
212
213    private void initLocked() {
214        Slog.d(TAG, "initLocked()");
215
216        mVpn.setEnableTeardown(false);
217
218        final IntentFilter resetFilter = new IntentFilter(ACTION_LOCKDOWN_RESET);
219        mContext.registerReceiver(mResetReceiver, resetFilter, CONNECTIVITY_INTERNAL, null);
220
221        try {
222            // TODO: support non-standard port numbers
223            mNetService.setFirewallEgressDestRule(mProfile.server, 500, true);
224            mNetService.setFirewallEgressDestRule(mProfile.server, 4500, true);
225            mNetService.setFirewallEgressDestRule(mProfile.server, 1701, true);
226        } catch (RemoteException e) {
227            throw new RuntimeException("Problem setting firewall rules", e);
228        }
229
230        synchronized (mStateLock) {
231            handleStateChangedLocked();
232        }
233    }
234
235    public void shutdown() {
236        synchronized (mStateLock) {
237            shutdownLocked();
238        }
239    }
240
241    private void shutdownLocked() {
242        Slog.d(TAG, "shutdownLocked()");
243
244        mAcceptedEgressIface = null;
245        mErrorCount = 0;
246
247        mVpn.stopLegacyVpn();
248        try {
249            mNetService.setFirewallEgressDestRule(mProfile.server, 500, false);
250            mNetService.setFirewallEgressDestRule(mProfile.server, 4500, false);
251            mNetService.setFirewallEgressDestRule(mProfile.server, 1701, false);
252        } catch (RemoteException e) {
253            throw new RuntimeException("Problem setting firewall rules", e);
254        }
255        clearSourceRulesLocked();
256        hideNotification();
257
258        mContext.unregisterReceiver(mResetReceiver);
259        mVpn.setEnableTeardown(true);
260    }
261
262    public void reset() {
263        Slog.d(TAG, "reset()");
264        synchronized (mStateLock) {
265            // cycle tracker, reset error count, and trigger retry
266            shutdownLocked();
267            initLocked();
268            handleStateChangedLocked();
269        }
270    }
271
272    private void clearSourceRulesLocked() {
273        try {
274            if (mAcceptedIface != null) {
275                mNetService.setFirewallInterfaceRule(mAcceptedIface, false);
276                mAcceptedIface = null;
277            }
278            if (mAcceptedSourceAddr != null) {
279                for (LinkAddress addr : mAcceptedSourceAddr) {
280                    mNetService.setFirewallEgressSourceRule(addr.toString(), false);
281                }
282                mAcceptedSourceAddr = null;
283            }
284        } catch (RemoteException e) {
285            throw new RuntimeException("Problem setting firewall rules", e);
286        }
287    }
288
289    public void onNetworkInfoChanged() {
290        synchronized (mStateLock) {
291            handleStateChangedLocked();
292        }
293    }
294
295    public void onVpnStateChanged(NetworkInfo info) {
296        if (info.getDetailedState() == DetailedState.FAILED) {
297            mErrorCount++;
298        }
299        synchronized (mStateLock) {
300            handleStateChangedLocked();
301        }
302    }
303
304    public NetworkInfo augmentNetworkInfo(NetworkInfo info) {
305        if (info.isConnected()) {
306            final NetworkInfo vpnInfo = mVpn.getNetworkInfo();
307            info = new NetworkInfo(info);
308            info.setDetailedState(vpnInfo.getDetailedState(), vpnInfo.getReason(), null);
309        }
310        return info;
311    }
312
313    private void showNotification(int titleRes, int iconRes) {
314        final Notification.Builder builder = new Notification.Builder(mContext)
315                .setWhen(0)
316                .setSmallIcon(iconRes)
317                .setContentTitle(mContext.getString(titleRes))
318                .setContentText(mContext.getString(R.string.vpn_lockdown_config))
319                .setContentIntent(mConfigIntent)
320                .setPriority(Notification.PRIORITY_LOW)
321                .setOngoing(true)
322                .addAction(R.drawable.ic_menu_refresh, mContext.getString(R.string.reset),
323                        mResetIntent)
324                .setColor(mContext.getResources().getColor(
325                        com.android.internal.R.color.system_notification_accent_color));
326
327        NotificationManager.from(mContext).notify(TAG, 0, builder.build());
328    }
329
330    private void hideNotification() {
331        NotificationManager.from(mContext).cancel(TAG, 0);
332    }
333}
334