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