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