Condivisione della rete Thread con le API delle credenziali Thread di Google

1. Prima di iniziare

Nel nostro codelab Thread Border Router (TBR), mostriamo come creare un router di confine Thread basato su un Raspberry Pi. In questo codelab

  • Stabilisci una connettività IP bidirezionale tra le reti Thread e Wi-Fi/Ethernet.
  • Fornisci il rilevamento dei servizi bidirezionale tramite mDNS (su collegamento Wi-Fi/Ethernet) e SRP (sulla rete Thread).

Questo codelab si basa su quello precedente, spiegando in che modo il tuo router di confine e la tua app possono interagire con le API di Google per creare un'unica rete Thread. La convergenza delle credenziali Thread è importante, in quanto aumenta la robustezza della rete e semplifica le interazioni degli utenti con le applicazioni che si basano su Thread.

Prerequisiti

  • Completa il codelab OTBR
  • Conoscenza di base del networking Linux, Android/Kotlin e Thread

Cosa imparerai a fare

  • Utilizzare le API di condivisione Thread per ottenere e impostare set di credenziali
  • Come configurare il tuo router di confine OpenThread con le stesse credenziali della rete Google

Che cosa ti serve

  • Scheda Raspberry Pi 4 o un'altra scheda basata su Linux che esegue il router di confine Open Thread (OTBR)
  • Scheda che fornisce connettività IEEE 802.15.4 come co-processore radiofonico (RCP). Consulta un elenco di repository di diversi fornitori di SoC e le relative istruzioni nella pagina GitHub di OpenThread

2. Configurazione del servizio HTTP

Il primo componente di base di cui abbiamo bisogno è un'interfaccia che ci consenta di leggere le credenziali attive e scrivere quelle in sospeso nell'OTBR. Quando crei un TBR, usa i tuoi meccanismi proprietari, come mostrato qui con due esempi. La prima opzione mostra come interfacciarsi con l'agente OTBR localmente tramite DBUS, mentre la seconda utilizza l'API Rest, che può essere creata su OTBR.

Nessuno dei due metodi è sicuro e non deve essere utilizzato così com'è in un ambiente di produzione. Tuttavia, un fornitore può creare la crittografia in base a entrambi i metodi per utilizzarlo in un ambiente di produzione oppure puoi estendere il tuo servizio di monitoraggio per emettere chiamate HTTP di loopback o DBUS intrinsecamente locali.

Opzione 1: DBUS e API HTTP su script Python

91e5fdeed83e9354.png

Questo passaggio crea un servizio HTTP essenziale che espone due endpoint per leggere e impostare le credenziali, chiamando in ultima analisi i comandi DBUS.

Nell'RPi che fungerà da OTBR, installa le dipendenze Python 3:

$ pip install dbus-python shlex json

Esegui lo script come segue:

$  sudo python credentials_server.py 8081
serving at port 8081

Nell'esempio viene configurato un server HTTP sulla porta 8081 e rimane in ascolto sul percorso principale per una richiesta GET per recuperare le credenziali di Thread o per una richiesta POST per impostare le credenziali di Thread. Il payload è sempre una struttura JSON con TLV.

La seguente richiesta PUT imposta nuove credenziali Thread in attesa sull'OTBR tramite il percorso /node/dataset/pending. In questo caso, le credenziali in attesa vengono applicate entro 10 secondi:

PUT /node/dataset/pending
Host: <IP>:8081
ContentType: "application/json"
acceptMimeType: "application/json"
...
{
        "ActiveDataset": "<TLV encoded new Thread Dataset>"
"PendingTimestamp": {
        "Seconds": <Unix timestamp in seconds>,
        "Ticks": 0,
        "Authoritative": false
},
"Delay": 10000 // in milliseconds
}

Una richiesta GET a /node/dataset/active recupera le credenziali attualmente attive.

GET /node/dataset/active
Host: <IP>:8081
ContentType = "application/json"
acceptMimeType = "text/plain"
...
<TLV encoded Thread Dataset>

Lo script chiama i comandi DBUS R/W sul percorso bus io.openthread.BorderRouter.wpan0, percorso oggetto /io/openthread/BorderRouter/wpan0

# D-BUS interface
def call_dbus_method(interface, method_name, *arguments):
    bus = dbus.SystemBus()
    obj = bus.get_object('io.openthread.BorderRouter.wpan0', '/io/openthread/BorderRouter/wpan0')
    iface = dbus.Interface(obj, interface)
    method = getattr(iface, method_name)
    res = method(*arguments)
    return res

def get_dbus_property(property_name):
    return call_dbus_method('org.freedesktop.DBus.Properties', 'Get', 'io.openthread.BorderRouter',
                                 property_name)

def set_dbus_property(property_name, property_value):
    return call_dbus_method('org.freedesktop.DBus.Properties', 'Set', 'io.openthread.BorderRouter',
                                 property_name, property_value)                               

DBUS consente l'introspezione delle sue capacità. Puoi procedere come segue:

$ sudo dbus-send --system --dest=io.openthread.BorderRouter.wpan0 \
        --type=method_call --print-reply /io/openthread/BorderRouter/wpan0 \
        org.freedesktop.DBus.Introspectable.Introspect

Puoi anche controllare le funzionalità supportate documentate qui.

Opzione 2: API Rest HTTP nativa dell'agente OTBR

c748ca5151b6cacb.png

Il router di confine OpenThread crea per impostazione predefinita il flag REST_API=1, abilitando l'API REST. Se la tua build da un codelab precedente non ha abilitato l'API REST, assicurati di creare OTBR sulla tua RPi con questo flag:

$ REST_API=1 INFRA_IF_NAME=wlan0 ./script/setup

Un agente OTBR può essere riavviato eseguendo:

$ sudo systemctl restart otbr-agent.service

L'agente avvia un server HTTP sulla porta 8081. Questo server consente a un utente o a un programma di monitoraggio di eseguire molte attività in OTBR (documentato qui). Puoi usare il browser, curl o wget per esaminarne i contenuti. Tra i tanti percorsi supportati ci sono i casi d'uso descritti sopra, con il verbo GET su /node/dataset/active e il verbo PUT su /node/dataset/pending

3. Configurare il framework delle credenziali su Android

Credenziali preferite

Google Play Services su Android consente e richiede la registrazione delle credenziali per tutti i TBR della rete. Ciascuno è identificato dal relativo ID Border Router Agent (BAID). Per eseguire questa attività, utilizzerai il metodo addCredentials() dell'interfaccia ThreadNetworkClient. Il primo TBR aggiunto allo spazio di archiviazione di Google Play Services determina le credenziali preferite per questo dispositivo mobile.

L'app che aggiunge un set di credenziali di rete Thread al proprio BAID diventa la proprietaria delle credenziali e dispone di autorizzazioni complete per accedervi. Se provi ad accedere alle credenziali aggiunte da altre app, riceverai un errore PERMISSION_DENIED. Tuttavia, le credenziali preferite sono sempre disponibili per qualsiasi app previo consenso dell'utente. Ti consigliamo di tenere aggiornate le credenziali archiviate in Google Play Services quando viene aggiornata la rete del router di confine Thread. Anche se queste informazioni non vengono utilizzate oggi, in futuro potremmo fornire percorsi più mirati.

Anche se il primo TBR viene escluso in un secondo momento, le Credenziali preferite continueranno a essere applicate al dispositivo Android. Una volta impostate, altre app che gestiscono le credenziali Thread potrebbero ottenere le credenziali da una chiamata getPreferredCredentials().

Sincronizzazione TBR di Google

I dispositivi Android si sincronizzano automaticamente con i TBR Google. Se non esistono credenziali su Android, i dispositivi le estraggono dai TBR Google nella tua rete e diventano le credenziali preferite. La sincronizzazione tra i TBR e il dispositivo Android avviene solo se il TBR è accoppiato a un singolo utente o se è accoppiato a due utenti che si trovano nella stessa smart home ( Struttura).

Questo processo si verifica anche quando un altro utente Google si trova in GHA per Android o GHA per iOS quando l'utente si trova nella stessa struttura. Nel caso di GHA per iOS, le credenziali preferite vengono impostate sullo spazio di archiviazione iOS, se non esistono credenziali preferite.

Se nella stessa rete esistono due dispositivi Android (o Android + iGHA) con insiemi diversi di credenziali preferite, il dispositivo che ha configurato in origine il TBR prevarrà sul TBR.

Operazioni preliminari per TBR di terze parti

L'archiviazione delle credenziali non è attualmente limitata alla smart home dell'utente ( Struttura). Ogni dispositivo Android avrà il proprio spazio di archiviazione BAID, ma una volta presente un TBR Google nella rete, gli altri dispositivi Android e iOS che eseguono l'app Google Home per iOS si sincronizzeranno con quel TBR e proveranno a impostare credenziali locali sullo spazio di archiviazione del telefono.

Prima che un nuovo OOB TBR crei una rete, è importante verificare se una rete preferita esiste già nello spazio di archiviazione di Android.

  • Se esiste una rete preferita, il fornitore dovrebbe utilizzarla. In questo modo, i dispositivi Thread sono connessi a una singola rete Thread quando possibile.
  • Quando non esiste una rete preferita, crea un nuovo set di credenziali e assegnalo al tuo TBR in Google Play Services. Android rispetterà queste credenziali come credenziali standard impostate su tutti i TBR basati su Google e altri fornitori saranno in grado di migliorare la copertura e la robustezza della tua rete mesh con dispositivi aggiuntivi

cd8bc726f67b1fa1.png

4. Clonazione e modifica dell'app per Android

Abbiamo creato un'app per Android che mostra le principali chiamate possibili all'API Thread. Puoi usare questi pattern nella tua app. In questo codelab cloneremo l'app di esempio di Google Home per Matter da GitHub.

Tutto il codice sorgente mostrato qui è già codificato nell'app di esempio. Ti invitiamo a modificarlo in base alle tue esigenze, ma puoi semplicemente clonare l'app o eseguire i file binari predefiniti per ispezionare la funzionalità.

  1. Clonalo utilizzando:
$ git clone https://github.com/google-home/sample-apps-for-matter-android.git
  1. Scarica e apri Android Studio.
  2. Fai clic su File > Apri e posiziona il puntatore del mouse sul repository clonato.
  3. Attiva la modalità sviluppatore sul tuo smartphone Android.
  4. Collegalo al computer tramite un cavo USB.
  5. Esegui l'app da Android Studio tramite <Cmd+R> (OS X) o <Ctrl+R> (Win, Linux)
  6. Vai alla ruota -> Utilità per sviluppatori -> Rete Thread
  7. Interagisci con le diverse opzioni disponibili. Nelle sezioni seguenti illustreremo il codice che viene eseguito su ogni pulsante.

Esistono le credenziali preferite?

La prima domanda che un produttore di TBR dovrebbe chiedere a Google è se nel dispositivo esiste già un set di credenziali preferito. Questo dovrebbe essere il punto di partenza del flusso. Il codice seguente esegue query sul GPS sull'esistenza delle credenziali. Non richiede il consenso dell'utente perché non viene condivisa alcuna credenziale.

/**
* Prompts whether credentials exist in storage or not. Consent from user is not necessary
*/

fun doGPSPreferredCredsExist(activity: FragmentActivity) {
 try {
   // Uses the ThreadNetwork interface for the preferred credentials, adding
   // a listener that will receive an intentSenderResult. If that is NULL, 
   // preferred credentials don't exist. If that isn't NULL, they exist.
   // In this case we'll not use it.

   ThreadNetwork.getClient(activity).preferredCredentials.addOnSuccessListener { intentSenderResult ->
     intentSenderResult.intentSender?.let { intentSender ->
       ToastTimber.d("threadClient: preferred credentials exist", activity)
       // don't post the intent on `threadClientIntentSender` as we do when
       // we really want to know which are the credentials. That will prompt a
       // user consent. In this case we just want to know whether they exist
     } ?: ToastTimber.d(
       "threadClient: no preferred credentials found, or no thread module found", activity
     )
   }.addOnFailureListener { e: Exception ->
     Timber.d("ERROR: [${e}]")
   }
 } catch (e: Exception) {
   ToastTimber.e("Error $e", activity)
 }
}

Recupero delle credenziali preferite GPS

Nel caso in cui esistano, vuoi leggere le credenziali. L'unica differenza rispetto al codice precedente è che, dopo aver ricevuto intentSenderResult, vuoi creare e avviare un intent utilizzando il risultato del mittente.

Nel nostro codice, a fini di organizzazione o architettura utilizziamo MutableLiveData<IntentSender?> perché il codice originale si trova nel ViewModel (ThreadViewModel.kt) e gli osservatori di intent si trovano nel frammento di attività ( ThreadFragment.kt). Pertanto, una volta pubblicato l'intentSenderResult nei dati in tempo reale, eseguiremo i contenuti di questo osservatore:

viewModel.threadClientIntentSender.observe(viewLifecycleOwner) { sender ->
 Timber.d(
   "threadClient: intent observe is called with [${intentSenderToString(sender)}]"
 )
 if (sender != null) {
   Timber.d("threadClient: Launch GPS activity to get ThreadClient")
   threadClientLauncher.launch(IntentSenderRequest.Builder(sender).build())
   viewModel.consumeThreadClientIntentSender()
 }
}

In questo modo, il consenso degli utenti verrà attivato con le credenziali di condivisione e, se approvato, i contenuti verranno restituiti tramite:

threadClientLauncher =
 registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { result ->
   if (result.resultCode == RESULT_OK) {
     val threadNetworkCredentials =
       ThreadNetworkCredentials.fromIntentSenderResultData(result.data!!)
     viewModel.threadPreferredCredentialsOperationalDataset.postValue(
       threadNetworkCredentials
     )
   } else {
     val error = "User denied request."
     Timber.d(error)
     updateThreadInfo(null, "")
   }
 }

La pubblicazione delle credenziali su MutableLiveData<ThreadNetworkCredentials?> è descritta di seguito.

Impostazione delle credenziali GPS

Che esista o meno, devi registrare il tuo TBR in Google Play Services. La tua app sarà l'unica in grado di leggere le credenziali associate all'ID agente di frontiera del tuo TBR, ma se il TBR è la prima a registrarsi, queste credenziali verranno copiate nel set di credenziali preferite. Queste informazioni sono accessibili a qualsiasi App sullo smartphone, con l'autorizzazione dell'utente.

/**
* Last step in setting the GPS thread credentials of a TBR
*/
private fun associateGPSThreadCredentialsToThreadBorderRouterAgent(
 credentials: ThreadNetworkCredentials?,
 activity: FragmentActivity,
 threadBorderAgent: ThreadBorderAgent,
) {
 credentials?.let {
   ThreadNetwork.getClient(activity).addCredentials(threadBorderAgent, credentials)
     .addOnSuccessListener {
       ToastTimber.d("threadClient: Credentials added", activity)
     }.addOnFailureListener { e: Exception ->
       ToastTimber.e("threadClient: Error adding the new credentials: $e", activity)
     }
 }
}

Impostare le credenziali per il prodotto TBR

Questa parte è di proprietà di ciascun fornitore e in questo codelab la implementiamo tramite DBUS + Python HTTP Rest Server o il server HTTP Rest nativo di OTBR.

/**
* Creates credentials in the format used by the OTBR HTTP server. See its documentation in
* https://github.com/openthread/ot-br-posix/blob/main/src/rest/openapi.yaml#L215
*/
fun createJsonCredentialsObject(newCredentials: ThreadNetworkCredentials): JSONObject {
 val jsonTimestamp = JSONObject()
 jsonTimestamp.put("Seconds", System.currentTimeMillis() / 1000)
 jsonTimestamp.put("Ticks", 0)
 jsonTimestamp.put("Authoritative", false)

 val jsonQuery = JSONObject()
 jsonQuery.put(
   "ActiveDataset",
   BaseEncoding.base16().encode(newCredentials.activeOperationalDataset)
 )
 jsonQuery.put("PendingTimestamp", jsonTimestamp)
 // delay of committing the pending set into active set: 10000ms
 jsonQuery.put("Delay", 10000)

 Timber.d(jsonQuery.toString())

 return jsonQuery
}

//(...)

var response = OtbrHttpClient.createJsonHttpRequest(
 URL("http://$ipAddress:$otbrPort$otbrDatasetPendingEndpoint"),
 activity,
 OtbrHttpClient.Verbs.PUT,
 jsonQuery.toString()
)

Ottenere le credenziali dal prodotto TBR

Come mostrato in precedenza, utilizza il verbo HTTP GET per ottenere le credenziali dal tuo TBR. Consulta lo script Python di esempio.

Creazione e importazioni

Quando crei la tua app per Android, devi apportare modifiche al file manifest, generare e importare le app per supportare il modulo Thread di Google Play Services. I tre snippet seguenti riassumono la maggior parte delle aggiunte.

Tieni presente che la nostra app di esempio è stata progettata principalmente per la messa in servizio di Matter. Di conseguenza, i relativi file Manifest e Gradle sono più complessi delle aggiunte necessarie per il solo utilizzo delle credenziali Thread.

Modifiche del file manifest

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    (...)
    <!-- usesCleartextTraffic needed for OTBR local unencrypted communication -->
    <!-- Not needed for Thread Module, only used for HTTP -->
    <uses-feature
    (...)
        android:usesCleartextTraffic="true">

    <application>
    (...)
    <!-- GPS automatically downloads scanner module when app is installed -->
    <!-- Not needed for Thread Module, only used for scanning QR Codes -->
    <meta-data
        android:name="com.google.mlkit.vision.DEPENDENCIES"
        android:value="barcode_ui"/>
    </application>
</manifest>

Build.gradle

// Thread Network
implementation 'com.google.android.gms:play-services-threadnetwork:16.0.0'
// Thread QR Code Scanning
implementation 'com.google.android.gms:play-services-code-scanner:16.0.0'
// Thread QR Code Generation
implementation 'com.journeyapps:zxing-android-embedded:4.1.0'
// Needed for using BaseEncoding class
implementation 'com.google.guava:guava:31.1-jre'

Importazioni pertinenti

// Thread Network Module
import com.google.android.gms.threadnetwork.ThreadNetworkCredentials
import com.google.android.gms.threadnetwork.ThreadBorderAgent
import com.google.android.gms.threadnetwork.ThreadNetwork

// Conversion of credentials to/fro Base16 (hex)
import com.google.common.io.BaseEncoding

// HTTP
import java.io.BufferedInputStream
import java.io.InputStream
import java.net.HttpURLConnection
import java.net.URL
import java.nio.charset.StandardCharsets

// Co-routines for HTTP calls
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch


// JSON
import org.json.JSONObject

// Logs
import timber.log.Timber

// mDNS/SD
import android.net.nsd.NsdServiceInfo

// QR Code reader / writer
import com.google.mlkit.vision.barcode.common.Barcode
import com.google.mlkit.vision.codescanner.GmsBarcodeScannerOptions
import com.google.mlkit.vision.codescanner.GmsBarcodeScanning
import com.google.zxing.BarcodeFormat
import com.google.zxing.MultiFormatWriter
import com.journeyapps.barcodescanner.BarcodeEncoder

5. Rilevamento mDNS/SD

La nostra app di esempio utilizza il rilevamento mDNS/SD per creare un elenco dei router di confine Thread disponibili nella rete, nonché i rispettivi BAID.

Ciò è molto utile quando inserisci le informazioni del tuo TBR nello spazio di archiviazione delle credenziali GPS. Tuttavia, il suo utilizzo esula dall'ambito di questo codelab. Utilizziamo la libreria Android Service Discovery NSDManager e il codice sorgente completo è disponibile nell'app di esempio in ServiceDiscovery.kt.

6. Riepilogo

Dopo aver implementato queste chiamate o aver utilizzato l'app di esempio, puoi eseguire l'onboarding completo del tuo OTBR RPi. La nostra app di esempio contiene 8 pulsanti:

91979bf065e9673d.png

Una possibile sequenza per l'onboarding del TBR è:

  1. Esegui una query per verificare se esistono credenziali preferenziali (blu, prima riga)
  2. In base alla risposta
  3. Ottieni le credenziali GPS preferite (blu, 2a riga)
  4. Imposta le credenziali TBR nel GPS (blu, 3a riga) -> Seleziona il tuo TBR -> Crea casuale -> Inserisci il nome della rete -> OK
  5. Ora che hai le credenziali preferite, impostale sul tuo OTBR utilizzando l'opzione Imposta le credenziali OTBR RPi, che applicherà le credenziali al set in attesa.

L'impostazione predefinita per l'app di esempio prevede l'uso di un ritardo di 10 secondi. Di conseguenza, dopo questo periodo, le credenziali del tuo TBR RPi (e di altri nodi che potrebbero esistere sulla sua rete) verranno migrate al nuovo set di dati.

7. Conclusione

In questo codelab abbiamo clonato un'app per Android di esempio e analizzato diversi snippet di codice che utilizzano i servizi Google Play Services API Thread Storage. Abbiamo utilizzato queste API per avere un set di dati comune su cui possiamo eseguire l'onboarding in un TBR RPi, che mostra il TBR di un fornitore.

Avere tutti i TBR di un utente nella stessa rete migliora la resilienza e la copertura della rete Thread. Impedisce inoltre i percorsi degli utenti imperfetti in cui le app non possono eseguire l'onboarding dei dispositivi Thread perché non hanno accesso alle credenziali.

Ci auguriamo che questo codelab e le app di esempio ti aiutino a progettare e sviluppare la tua app e il tuo prodotto router di confine Thread.

8. Riferimenti

Co-processore RCP

DBUS