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