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