• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 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.server.thread;
18 
19 import android.annotation.Nullable;
20 import android.content.Context;
21 import android.net.thread.ActiveOperationalDataset;
22 import android.net.thread.IConfigurationReceiver;
23 import android.net.thread.IOperationReceiver;
24 import android.net.thread.IOutputReceiver;
25 import android.net.thread.OperationalDatasetTimestamp;
26 import android.net.thread.PendingOperationalDataset;
27 import android.net.thread.ThreadConfiguration;
28 import android.net.thread.ThreadNetworkException;
29 import android.os.Binder;
30 import android.os.Process;
31 import android.text.TextUtils;
32 
33 import com.android.internal.annotations.VisibleForTesting;
34 import com.android.modules.utils.BasicShellCommandHandler;
35 import com.android.net.module.util.HexDump;
36 
37 import java.io.PrintWriter;
38 import java.time.Duration;
39 import java.time.Instant;
40 import java.util.concurrent.CompletableFuture;
41 import java.util.concurrent.ExecutionException;
42 import java.util.concurrent.TimeUnit;
43 import java.util.concurrent.TimeoutException;
44 
45 /**
46  * Interprets and executes 'adb shell cmd thread_network <subcommand>'.
47  *
48  * <p>Subcommands which don't have an equivalent Java API now require the
49  * "android.permission.THREAD_NETWORK_TESTING" permission. For a specific subcommand, it also
50  * requires the same permissions of the equivalent Java / AIDL API.
51  *
52  * <p>To add new commands: - onCommand: Add a case "<command>" execute. Return a 0 if command
53  * executed successfully. - onHelp: add a description string.
54  */
55 public final class ThreadNetworkShellCommand extends BasicShellCommandHandler {
56     private static final Duration SET_ENABLED_TIMEOUT = Duration.ofSeconds(2);
57     private static final Duration LEAVE_TIMEOUT = Duration.ofSeconds(2);
58     private static final Duration MIGRATE_TIMEOUT = Duration.ofSeconds(2);
59     private static final Duration FORCE_STOP_TIMEOUT = Duration.ofSeconds(1);
60     private static final Duration OT_CTL_COMMAND_TIMEOUT = Duration.ofSeconds(5);
61     private static final Duration CONFIG_TIMEOUT = Duration.ofSeconds(1);
62     private static final String PERMISSION_THREAD_NETWORK_TESTING =
63             "android.permission.THREAD_NETWORK_TESTING";
64 
65     private final Context mContext;
66     private final ThreadNetworkControllerService mControllerService;
67     private final ThreadNetworkCountryCode mCountryCode;
68 
69     @Nullable private PrintWriter mOutputWriter;
70     @Nullable private PrintWriter mErrorWriter;
71 
72     @VisibleForTesting
ThreadNetworkShellCommand( Context context, ThreadNetworkControllerService controllerService, ThreadNetworkCountryCode countryCode)73     ThreadNetworkShellCommand(
74             Context context,
75             ThreadNetworkControllerService controllerService,
76             ThreadNetworkCountryCode countryCode) {
77         mContext = context;
78         mControllerService = controllerService;
79         mCountryCode = countryCode;
80     }
81 
82     @VisibleForTesting
setPrintWriters(PrintWriter outputWriter, PrintWriter errorWriter)83     public void setPrintWriters(PrintWriter outputWriter, PrintWriter errorWriter) {
84         mOutputWriter = outputWriter;
85         mErrorWriter = errorWriter;
86     }
87 
isRootProcess()88     private static boolean isRootProcess() {
89         return Binder.getCallingUid() == Process.ROOT_UID;
90     }
91 
getOutputWriter()92     private PrintWriter getOutputWriter() {
93         return (mOutputWriter != null) ? mOutputWriter : getOutPrintWriter();
94     }
95 
getErrorWriter()96     private PrintWriter getErrorWriter() {
97         return (mErrorWriter != null) ? mErrorWriter : getErrPrintWriter();
98     }
99 
100     @Override
onHelp()101     public void onHelp() {
102         final PrintWriter pw = getOutputWriter();
103         pw.println("Thread network commands:");
104         pw.println("  help or -h");
105         pw.println("    Print this help text.");
106         pw.println("  enable");
107         pw.println("    Enables Thread radio");
108         pw.println("  disable");
109         pw.println("    Disables Thread radio");
110         pw.println("  join <active-dataset-tlvs>");
111         pw.println("    Joins a network of the given dataset");
112         pw.println("  migrate <active-dataset-tlvs> <delay-seconds>");
113         pw.println("    Migrate to the given network by a specific delay");
114         pw.println("  leave");
115         pw.println("    Leave the current network and erase datasets");
116         pw.println("  force-stop-ot-daemon enabled | disabled ");
117         pw.println("    force stop ot-daemon service");
118         pw.println("  get-country-code");
119         pw.println("    Gets country code as a two-letter string");
120         pw.println("  force-country-code enabled <two-letter code> | disabled ");
121         pw.println("    Sets country code to <two-letter code> or left for normal value");
122         pw.println("  ot-ctl <subcommand>");
123         pw.println("    Runs ot-ctl command");
124         pw.println("  config [name] [value]");
125         pw.println("    Gets the config or sets the value for a config entry");
126     }
127 
128     @Override
onCommand(String cmd)129     public int onCommand(String cmd) {
130         // Treat no command as the "help" command
131         if (TextUtils.isEmpty(cmd)) {
132             cmd = "help";
133         }
134 
135         switch (cmd) {
136             case "enable":
137                 return setThreadEnabled(true);
138             case "disable":
139                 return setThreadEnabled(false);
140             case "config":
141                 return handleConfigCommand();
142             case "join":
143                 return join();
144             case "leave":
145                 return leave();
146             case "migrate":
147                 return migrate();
148             case "force-stop-ot-daemon":
149                 return forceStopOtDaemon();
150             case "force-country-code":
151                 return forceCountryCode();
152             case "get-country-code":
153                 return getCountryCode();
154             case "ot-ctl":
155                 return handleOtCtlCommand();
156             default:
157                 return handleDefaultCommands(cmd);
158         }
159     }
160 
ensureTestingPermission()161     private void ensureTestingPermission() {
162         mContext.enforceCallingOrSelfPermission(
163                 PERMISSION_THREAD_NETWORK_TESTING,
164                 "Permission " + PERMISSION_THREAD_NETWORK_TESTING + " is missing!");
165     }
166 
setThreadEnabled(boolean enabled)167     private int setThreadEnabled(boolean enabled) {
168         CompletableFuture<Void> setEnabledFuture = new CompletableFuture<>();
169         mControllerService.setEnabled(enabled, newOperationReceiver(setEnabledFuture));
170         return waitForFuture(setEnabledFuture, SET_ENABLED_TIMEOUT, getErrorWriter());
171     }
172 
join()173     private int join() {
174         byte[] datasetTlvs = HexDump.hexStringToByteArray(getNextArgRequired());
175         ActiveOperationalDataset dataset;
176         try {
177             dataset = ActiveOperationalDataset.fromThreadTlvs(datasetTlvs);
178         } catch (IllegalArgumentException e) {
179             getErrorWriter().println("Invalid dataset argument: " + e.getMessage());
180             return -1;
181         }
182         // Do not wait for join to complete because this can take 8 to 30 seconds
183         mControllerService.join(dataset, new IOperationReceiver.Default());
184         return 0;
185     }
186 
leave()187     private int leave() {
188         CompletableFuture<Void> leaveFuture = new CompletableFuture<>();
189         mControllerService.leave(newOperationReceiver(leaveFuture));
190         return waitForFuture(leaveFuture, LEAVE_TIMEOUT, getErrorWriter());
191     }
192 
migrate()193     private int migrate() {
194         byte[] datasetTlvs = HexDump.hexStringToByteArray(getNextArgRequired());
195         ActiveOperationalDataset dataset;
196         try {
197             dataset = ActiveOperationalDataset.fromThreadTlvs(datasetTlvs);
198         } catch (IllegalArgumentException e) {
199             getErrorWriter().println("Invalid dataset argument: " + e.getMessage());
200             return -1;
201         }
202 
203         int delaySeconds;
204         try {
205             delaySeconds = Integer.parseInt(getNextArgRequired());
206         } catch (NumberFormatException e) {
207             getErrorWriter().println("Invalid delay argument: " + e.getMessage());
208             return -1;
209         }
210 
211         PendingOperationalDataset pendingDataset =
212                 new PendingOperationalDataset(
213                         dataset,
214                         OperationalDatasetTimestamp.fromInstant(Instant.now()),
215                         Duration.ofSeconds(delaySeconds));
216         CompletableFuture<Void> migrateFuture = new CompletableFuture<>();
217         mControllerService.scheduleMigration(pendingDataset, newOperationReceiver(migrateFuture));
218         return waitForFuture(migrateFuture, MIGRATE_TIMEOUT, getErrorWriter());
219     }
220 
forceStopOtDaemon()221     private int forceStopOtDaemon() {
222         ensureTestingPermission();
223         final PrintWriter errorWriter = getErrorWriter();
224         boolean enabled;
225         try {
226             enabled = getNextArgRequiredTrueOrFalse("enabled", "disabled");
227         } catch (IllegalArgumentException e) {
228             errorWriter.println("Invalid argument: " + e.getMessage());
229             return -1;
230         }
231 
232         CompletableFuture<Void> forceStopFuture = new CompletableFuture<>();
233         mControllerService.forceStopOtDaemonForTest(enabled, newOperationReceiver(forceStopFuture));
234         return waitForFuture(forceStopFuture, FORCE_STOP_TIMEOUT, getErrorWriter());
235     }
236 
forceCountryCode()237     private int forceCountryCode() {
238         ensureTestingPermission();
239         final PrintWriter perr = getErrorWriter();
240         boolean enabled;
241         try {
242             enabled = getNextArgRequiredTrueOrFalse("enabled", "disabled");
243         } catch (IllegalArgumentException e) {
244             perr.println("Invalid argument: " + e.getMessage());
245             return -1;
246         }
247 
248         if (enabled) {
249             String countryCode = getNextArgRequired();
250             if (!ThreadNetworkCountryCode.isValidCountryCode(countryCode)) {
251                 perr.println(
252                         "Invalid argument: Country code must be a 2-letter"
253                                 + " string. But got country code "
254                                 + countryCode
255                                 + " instead");
256                 return -1;
257             }
258             mCountryCode.setOverrideCountryCode(countryCode);
259         } else {
260             mCountryCode.clearOverrideCountryCode();
261         }
262         return 0;
263     }
264 
getCountryCode()265     private int getCountryCode() {
266         ensureTestingPermission();
267         getOutputWriter().println("Thread country code = " + mCountryCode.getCountryCode());
268         return 0;
269     }
270 
handleConfigCommand()271     private int handleConfigCommand() {
272         ensureTestingPermission();
273 
274         // Get config
275         if (peekNextArg() == null) {
276             try {
277                 final ThreadConfiguration config = getConfig();
278                 getOutputWriter().println("Thread configuration = " + config);
279             } catch (AssertionError e) {
280                 getErrorWriter().println("Failed: " + e.getMessage());
281                 return -1;
282             }
283             return 0;
284         }
285 
286         // Set config
287         final String name = getNextArg();
288         final String value = getNextArg();
289         try {
290             setConfig(name, value);
291         } catch (AssertionError | IllegalArgumentException e) {
292             getErrorWriter().println(e.getMessage());
293             return -1;
294         }
295         return 0;
296     }
297 
getConfig()298     private ThreadConfiguration getConfig() throws AssertionError {
299         final CompletableFuture<ThreadConfiguration> future = new CompletableFuture<>();
300         mControllerService.registerConfigurationCallback(
301                 new IConfigurationReceiver.Stub() {
302                     @Override
303                     public void onConfigurationChanged(ThreadConfiguration config) {
304                         future.complete(config);
305                     }
306                 });
307         try {
308             return future.get(CONFIG_TIMEOUT.toSeconds(), TimeUnit.SECONDS);
309         } catch (InterruptedException | ExecutionException | TimeoutException e) {
310             throw new AssertionError("Failed to get config within timeout", e);
311         }
312     }
313 
setConfig(String name, String value)314     private void setConfig(String name, String value)
315             throws IllegalArgumentException, AssertionError {
316         if (name == null || value == null) {
317             throw new IllegalArgumentException(
318                     "Invalid config name = " + name + ", value=" + value);
319         }
320         final ThreadConfiguration oldConfig = getConfig();
321         final ThreadConfiguration.Builder newConfigBuilder =
322                 new ThreadConfiguration.Builder(oldConfig);
323         switch (name) {
324             case "br" -> newConfigBuilder.setBorderRouterEnabled(argEnabledOrDisabled(value));
325             case "nat64" -> newConfigBuilder.setNat64Enabled(argEnabledOrDisabled(value));
326             case "pd" -> newConfigBuilder.setDhcpv6PdEnabled(argEnabledOrDisabled(value));
327             default -> throw new IllegalArgumentException("Invalid config name: " + name);
328         }
329         CompletableFuture<Void> future = new CompletableFuture();
330         mControllerService.setConfiguration(newConfigBuilder.build(), newOperationReceiver(future));
331         waitForFuture(future, CONFIG_TIMEOUT, mErrorWriter);
332     }
333 
334     private static final class OutputReceiver extends IOutputReceiver.Stub {
335         private final CompletableFuture<Void> future;
336         private final PrintWriter outputWriter;
337 
OutputReceiver(CompletableFuture<Void> future, PrintWriter outputWriter)338         public OutputReceiver(CompletableFuture<Void> future, PrintWriter outputWriter) {
339             this.future = future;
340             this.outputWriter = outputWriter;
341         }
342 
343         @Override
onOutput(String output)344         public void onOutput(String output) {
345             outputWriter.print(output);
346             outputWriter.flush();
347         }
348 
349         @Override
onComplete()350         public void onComplete() {
351             future.complete(null);
352         }
353 
354         @Override
onError(int errorCode, String errorMessage)355         public void onError(int errorCode, String errorMessage) {
356             future.completeExceptionally(new ThreadNetworkException(errorCode, errorMessage));
357         }
358     }
359 
handleOtCtlCommand()360     private int handleOtCtlCommand() {
361         ensureTestingPermission();
362 
363         if (!isRootProcess()) {
364             getErrorWriter().println("No access to ot-ctl command");
365             return -1;
366         }
367 
368         final String subCommand = String.join(" ", peekRemainingArgs());
369 
370         CompletableFuture<Void> completeFuture = new CompletableFuture<>();
371         mControllerService.runOtCtlCommand(
372                 subCommand,
373                 false /* isInteractive */,
374                 new OutputReceiver(completeFuture, getOutputWriter()));
375         return waitForFuture(completeFuture, OT_CTL_COMMAND_TIMEOUT, getErrorWriter());
376     }
377 
newOperationReceiver(CompletableFuture<Void> future)378     private static IOperationReceiver newOperationReceiver(CompletableFuture<Void> future) {
379         return new IOperationReceiver.Stub() {
380             @Override
381             public void onSuccess() {
382                 future.complete(null);
383             }
384 
385             @Override
386             public void onError(int errorCode, String errorMessage) {
387                 future.completeExceptionally(new ThreadNetworkException(errorCode, errorMessage));
388             }
389         };
390     }
391 
392     /**
393      * Waits for the future to complete within given timeout.
394      *
395      * <p>Returns 0 if {@code future} completed successfully, or -1 if {@code future} failed to
396      * complete. When failed, error messages are printed to {@code errorWriter}.
397      */
398     private int waitForFuture(
399             CompletableFuture<Void> future, Duration timeout, PrintWriter errorWriter) {
400         try {
401             future.get(timeout.toSeconds(), TimeUnit.SECONDS);
402             return 0;
403         } catch (InterruptedException e) {
404             Thread.currentThread().interrupt();
405             errorWriter.println("Failed: " + e.getMessage());
406         } catch (ExecutionException e) {
407             errorWriter.println("Failed: " + e.getCause().getMessage());
408         } catch (TimeoutException e) {
409             errorWriter.println("Failed: command timeout for " + timeout);
410         }
411 
412         return -1;
413     }
414 
415     private static boolean argTrueOrFalse(String arg, String trueString, String falseString) {
416         if (trueString.equals(arg)) {
417             return true;
418         } else if (falseString.equals(arg)) {
419             return false;
420         } else {
421             throw new IllegalArgumentException(
422                     "Expected '"
423                             + trueString
424                             + "' or '"
425                             + falseString
426                             + "' as next arg but got '"
427                             + arg
428                             + "'");
429         }
430     }
431 
432     private static boolean argEnabledOrDisabled(String arg) {
433         return argTrueOrFalse(arg, "enabled", "disabled");
434     }
435 
436     private boolean getNextArgRequiredTrueOrFalse(String trueString, String falseString) {
437         String nextArg = getNextArgRequired();
438         return argTrueOrFalse(nextArg, trueString, falseString);
439     }
440 }
441