1 /*
2  * Copyright (C) 2021 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 
17 package com.android.net.module.util;
18 
19 import static android.net.INetd.IF_STATE_DOWN;
20 import static android.net.INetd.IF_STATE_UP;
21 import static android.net.RouteInfo.RTN_THROW;
22 import static android.net.RouteInfo.RTN_UNICAST;
23 import static android.net.RouteInfo.RTN_UNREACHABLE;
24 import static android.system.OsConstants.EBUSY;
25 
26 import android.annotation.SuppressLint;
27 import android.net.INetd;
28 import android.net.InterfaceConfigurationParcel;
29 import android.net.IpPrefix;
30 import android.net.RouteInfo;
31 import android.net.RouteInfoParcel;
32 import android.net.TetherConfigParcel;
33 import android.os.RemoteException;
34 import android.os.ServiceSpecificException;
35 import android.os.SystemClock;
36 import android.util.Log;
37 
38 import androidx.annotation.NonNull;
39 import androidx.annotation.VisibleForTesting;
40 
41 import java.util.ArrayList;
42 import java.util.Arrays;
43 import java.util.HashSet;
44 import java.util.List;
45 import java.util.Set;
46 
47 /**
48  * Collection of utilities for netd.
49  */
50 public class NetdUtils {
51     private static final String TAG = NetdUtils.class.getSimpleName();
52 
53     /** Used to modify the specified route. */
54     public enum ModifyOperation {
55         ADD,
56         REMOVE,
57     }
58 
59     /**
60      * Get InterfaceConfigurationParcel from netd.
61      */
getInterfaceConfigParcel(@onNull INetd netd, @NonNull String iface)62     public static InterfaceConfigurationParcel getInterfaceConfigParcel(@NonNull INetd netd,
63             @NonNull String iface) {
64         try {
65             return netd.interfaceGetCfg(iface);
66         } catch (RemoteException | ServiceSpecificException e) {
67             throw new IllegalStateException(e);
68         }
69     }
70 
validateFlag(String flag)71     private static void validateFlag(String flag) {
72         if (flag.indexOf(' ') >= 0) {
73             throw new IllegalArgumentException("flag contains space: " + flag);
74         }
75     }
76 
77     /**
78      * Check whether the InterfaceConfigurationParcel contains the target flag or not.
79      *
80      * @param config The InterfaceConfigurationParcel instance.
81      * @param flag Target flag string to be checked.
82      */
hasFlag(@onNull final InterfaceConfigurationParcel config, @NonNull final String flag)83     public static boolean hasFlag(@NonNull final InterfaceConfigurationParcel config,
84             @NonNull final String flag) {
85         validateFlag(flag);
86         final Set<String> flagList = new HashSet<String>(Arrays.asList(config.flags));
87         return flagList.contains(flag);
88     }
89 
90     @VisibleForTesting
removeAndAddFlags(@onNull String[] flags, @NonNull String remove, @NonNull String add)91     protected static String[] removeAndAddFlags(@NonNull String[] flags, @NonNull String remove,
92             @NonNull String add) {
93         final ArrayList<String> result = new ArrayList<>();
94         try {
95             // Validate the add flag first, so that the for-loop can be ignore once the format of
96             // add flag is invalid.
97             validateFlag(add);
98             for (String flag : flags) {
99                 // Simply ignore both of remove and add flags first, then add the add flag after
100                 // exiting the loop to prevent adding the duplicate flag.
101                 if (remove.equals(flag) || add.equals(flag)) continue;
102                 result.add(flag);
103             }
104             result.add(add);
105             return result.toArray(new String[result.size()]);
106         } catch (IllegalArgumentException iae) {
107             throw new IllegalStateException("Invalid InterfaceConfigurationParcel", iae);
108         }
109     }
110 
111     /**
112      * Set interface configuration to netd by passing InterfaceConfigurationParcel.
113      */
setInterfaceConfig(INetd netd, InterfaceConfigurationParcel configParcel)114     public static void setInterfaceConfig(INetd netd, InterfaceConfigurationParcel configParcel) {
115         try {
116             netd.interfaceSetCfg(configParcel);
117         } catch (RemoteException | ServiceSpecificException e) {
118             throw new IllegalStateException(e);
119         }
120     }
121 
122     /**
123      * Set the given interface up.
124      */
setInterfaceUp(INetd netd, String iface)125     public static void setInterfaceUp(INetd netd, String iface) {
126         final InterfaceConfigurationParcel configParcel = getInterfaceConfigParcel(netd, iface);
127         configParcel.flags = removeAndAddFlags(configParcel.flags, IF_STATE_DOWN /* remove */,
128                 IF_STATE_UP /* add */);
129         setInterfaceConfig(netd, configParcel);
130     }
131 
132     /**
133      * Set the given interface down.
134      */
setInterfaceDown(INetd netd, String iface)135     public static void setInterfaceDown(INetd netd, String iface) {
136         final InterfaceConfigurationParcel configParcel = getInterfaceConfigParcel(netd, iface);
137         configParcel.flags = removeAndAddFlags(configParcel.flags, IF_STATE_UP /* remove */,
138                 IF_STATE_DOWN /* add */);
139         setInterfaceConfig(netd, configParcel);
140     }
141 
142     /** Start tethering. */
tetherStart(final INetd netd, final boolean usingLegacyDnsProxy, final String[] dhcpRange)143     public static void tetherStart(final INetd netd, final boolean usingLegacyDnsProxy,
144             final String[] dhcpRange) throws RemoteException, ServiceSpecificException {
145         final TetherConfigParcel config = new TetherConfigParcel();
146         config.usingLegacyDnsProxy = usingLegacyDnsProxy;
147         config.dhcpRanges = dhcpRange;
148         netd.tetherStartWithConfiguration(config);
149     }
150 
151     /** Setup interface for tethering. */
tetherInterface(final INetd netd, final String iface, final IpPrefix dest)152     public static void tetherInterface(final INetd netd, final String iface, final IpPrefix dest)
153             throws RemoteException, ServiceSpecificException {
154         tetherInterface(netd, iface, dest, 20 /* maxAttempts */, 50 /* pollingIntervalMs */);
155     }
156 
157     /** Setup interface with configurable retries for tethering. */
tetherInterface(final INetd netd, final String iface, final IpPrefix dest, int maxAttempts, int pollingIntervalMs)158     public static void tetherInterface(final INetd netd, final String iface, final IpPrefix dest,
159             int maxAttempts, int pollingIntervalMs)
160             throws RemoteException, ServiceSpecificException {
161         netd.tetherInterfaceAdd(iface);
162         networkAddInterface(netd, iface, maxAttempts, pollingIntervalMs);
163         // Activate a route to dest and IPv6 link local.
164         modifyRoute(netd, ModifyOperation.ADD, INetd.LOCAL_NET_ID,
165                 new RouteInfo(dest, null, iface, RTN_UNICAST));
166         modifyRoute(netd, ModifyOperation.ADD, INetd.LOCAL_NET_ID,
167                 new RouteInfo(new IpPrefix("fe80::/64"), null, iface, RTN_UNICAST));
168     }
169 
170     /**
171      * Retry Netd#networkAddInterface for EBUSY error code.
172      * If the same interface (e.g., wlan0) is in client mode and then switches to tethered mode.
173      * There can be a race where puts the interface into the local network but interface is still
174      * in use in netd because the ConnectivityService thread hasn't processed the disconnect yet.
175      * See b/158269544 for detail.
176      */
networkAddInterface(final INetd netd, final String iface, int maxAttempts, int pollingIntervalMs)177     private static void networkAddInterface(final INetd netd, final String iface,
178             int maxAttempts, int pollingIntervalMs)
179             throws ServiceSpecificException, RemoteException {
180         for (int i = 1; i <= maxAttempts; i++) {
181             try {
182                 netd.networkAddInterface(INetd.LOCAL_NET_ID, iface);
183                 return;
184             } catch (ServiceSpecificException e) {
185                 if (e.errorCode == EBUSY && i < maxAttempts) {
186                     SystemClock.sleep(pollingIntervalMs);
187                     continue;
188                 }
189 
190                 Log.e(TAG, "Retry Netd#networkAddInterface failure: " + e);
191                 throw e;
192             }
193         }
194     }
195 
196     /** Reset interface for tethering. */
untetherInterface(final INetd netd, String iface)197     public static void untetherInterface(final INetd netd, String iface)
198             throws RemoteException, ServiceSpecificException {
199         try {
200             netd.tetherInterfaceRemove(iface);
201         } finally {
202             netd.networkRemoveInterface(INetd.LOCAL_NET_ID, iface);
203         }
204     }
205 
206     /** Add |routes| to local network. */
addRoutesToLocalNetwork(final INetd netd, final String iface, final List<RouteInfo> routes)207     public static void addRoutesToLocalNetwork(final INetd netd, final String iface,
208             final List<RouteInfo> routes) {
209 
210         for (RouteInfo route : routes) {
211             if (!route.isDefaultRoute()) {
212                 modifyRoute(netd, ModifyOperation.ADD, INetd.LOCAL_NET_ID, route);
213             }
214         }
215 
216         // IPv6 link local should be activated always.
217         modifyRoute(netd, ModifyOperation.ADD, INetd.LOCAL_NET_ID,
218                 new RouteInfo(new IpPrefix("fe80::/64"), null, iface, RTN_UNICAST));
219     }
220 
221     /** Remove routes from local network. */
removeRoutesFromLocalNetwork(final INetd netd, final List<RouteInfo> routes)222     public static int removeRoutesFromLocalNetwork(final INetd netd, final List<RouteInfo> routes) {
223         int failures = 0;
224 
225         for (RouteInfo route : routes) {
226             try {
227                 modifyRoute(netd, ModifyOperation.REMOVE, INetd.LOCAL_NET_ID, route);
228             } catch (IllegalStateException e) {
229                 failures++;
230             }
231         }
232 
233         return failures;
234     }
235 
236     @SuppressLint("NewApi")
findNextHop(final RouteInfo route)237     private static String findNextHop(final RouteInfo route) {
238         final String nextHop;
239         switch (route.getType()) {
240             case RTN_UNICAST:
241                 if (route.hasGateway()) {
242                     nextHop = route.getGateway().getHostAddress();
243                 } else {
244                     nextHop = INetd.NEXTHOP_NONE;
245                 }
246                 break;
247             case RTN_UNREACHABLE:
248                 nextHop = INetd.NEXTHOP_UNREACHABLE;
249                 break;
250             case RTN_THROW:
251                 nextHop = INetd.NEXTHOP_THROW;
252                 break;
253             default:
254                 nextHop = INetd.NEXTHOP_NONE;
255                 break;
256         }
257         return nextHop;
258     }
259 
260     /** Add or remove |route|. */
modifyRoute(final INetd netd, final ModifyOperation op, final int netId, final RouteInfo route)261     public static void modifyRoute(final INetd netd, final ModifyOperation op, final int netId,
262             final RouteInfo route) {
263         final String ifName = route.getInterface();
264         final String dst = route.getDestination().toString();
265         final String nextHop = findNextHop(route);
266 
267         try {
268             switch(op) {
269                 case ADD:
270                     netd.networkAddRoute(netId, ifName, dst, nextHop);
271                     break;
272                 case REMOVE:
273                     netd.networkRemoveRoute(netId, ifName, dst, nextHop);
274                     break;
275                 default:
276                     throw new IllegalStateException("Unsupported modify operation:" + op);
277             }
278         } catch (RemoteException | ServiceSpecificException e) {
279             throw new IllegalStateException(e);
280         }
281     }
282 
283     /**
284      * Convert a RouteInfo into a RouteInfoParcel.
285      */
toRouteInfoParcel(RouteInfo route)286     public static RouteInfoParcel toRouteInfoParcel(RouteInfo route) {
287         final String nextHop;
288 
289         switch (route.getType()) {
290             case RouteInfo.RTN_UNICAST:
291                 if (route.hasGateway()) {
292                     nextHop = route.getGateway().getHostAddress();
293                 } else {
294                     nextHop = INetd.NEXTHOP_NONE;
295                 }
296                 break;
297             case RouteInfo.RTN_UNREACHABLE:
298                 nextHop = INetd.NEXTHOP_UNREACHABLE;
299                 break;
300             case RouteInfo.RTN_THROW:
301                 nextHop = INetd.NEXTHOP_THROW;
302                 break;
303             default:
304                 nextHop = INetd.NEXTHOP_NONE;
305                 break;
306         }
307 
308         final RouteInfoParcel rip = new RouteInfoParcel();
309         rip.ifName = route.getInterface();
310         rip.destination = route.getDestination().toString();
311         rip.nextHop = nextHop;
312         rip.mtu = route.getMtu();
313 
314         return rip;
315     }
316 }
317