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