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