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

1. Prima di iniziare

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

  • Stabilisci la connettività IP bidirezionale tra le reti Thread e Wi-Fi/Ethernet.
  • Fornisce il rilevamento bidirezionale dei servizi tramite mDNS (sul link Wi-Fi/Ethernet) e SRP (sulla rete Thread).

Questo codelab si basa sul precedente e spiega in che modo il tuo router di confine e la tua app possono interagire con le API di Google per creare una singola rete Thread. La convergenza delle credenziali Thread è importante perché 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 di Linux, Android/Kotlin e del networking Thread

Obiettivi didattici

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

Che cosa ti serve

  • Scheda Raspberry Pi 4 o un'altra scheda basata su Linux su cui è in esecuzione il router di confine Open Thread (OTBR)
  • Scheda che fornisce connettività IEEE 802.15.4 come coprocessore radio (RCP). Consulta un elenco dei repository di diversi fornitori di SoC e le relative istruzioni nella pagina GitHub di OpenThread.

2. Configurazione del servizio HTTP

Il primo componente di cui abbiamo bisogno è un'interfaccia che ci consenta di leggere le credenziali attive e scrivere le credenziali in attesa nel tuo OTBR. Quando crei un TBR, utilizza 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 sfrutta l'API Rest che può essere creata sull'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 attorno a uno dei due metodi per utilizzarlo in un ambiente di produzione oppure puoi estendere il tuo servizio di monitoraggio per emettere chiamate DBUS loopback HTTP o intrinsecamente locali.

Opzione 1: API DBUS e HTTP in script Python

91e5fdeed83e9354.png

Questo passaggio crea un servizio HTTP di base che espone due endpoint per leggere e impostare le credenziali, chiamando infine i comandi DBUS.

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

$ pip install dbus-python shlex json

Esegui lo script come:

$  sudo python credentials_server.py 8081
serving at port 8081

L'esempio configura un server HTTP sulla porta 8081 e rimane in ascolto nel percorso principale per una richiesta GET per recuperare le credenziali di Thread o una richiesta POST per impostarle. Il payload è sempre una struttura JSON con il TLV.

La seguente richiesta PUT imposta nuove credenziali Thread in attesa nell'OTBR utilizzando il percorso /node/dataset/pending. In questo caso, le credenziali in attesa vengono applicate dopo 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 di lettura/scrittura DBUS per il percorso del bus io.openthread.BorderRouter.wpan0 e il percorso dell'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 funzionalità. Puoi farlo 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 viene compilato per impostazione predefinita con il flag REST_API=1, che attiva l'API REST. Se la compilazione di un codelab precedente non ha abilitato l'API REST, assicurati di compilare OTBR sulla 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 programma di monitoraggio o utente di eseguire molte attività nell'OTBR (documentato qui). Puoi utilizzare il browser, curl o wget per ispezionarne i contenuti. Tra i molti percorsi supportati sono inclusi i casi d'uso descritti sopra, con il verbo GET su /node/dataset/active e il verbo PUT su /node/dataset/pending.

3. Configurazione di Credential Framework su Android

Credenziali preferite

Google Play Services su Android consente e prevede la registrazione delle credenziali per tutti i TBR della tua rete. Ciascuno è identificato dal proprio ID agente router di confine (BAID). Per eseguire questa operazione, 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 insieme di credenziali di rete Thread al proprio BAID diventa il proprietario delle credenziali e dispone delle 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 con il consenso dell'utente. Ti consigliamo di mantenere aggiornate le credenziali memorizzate in Google Play Services quando la rete del router di confine Thread viene aggiornata. Anche se al momento queste informazioni non vengono utilizzate, in futuro potremmo offrire percorsi migliorati.

Anche se il primo TBR viene successivamente escluso, le credenziali preferite rimarranno sul dispositivo Android. Una volta impostate, altre app che gestiscono le credenziali Thread possono ottenerle da una chiamata getPreferredCredentials().

Google TBR Sync

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

Questo processo verrà eseguito anche quando un altro utente Google utilizza GHA per Android o GHA per iOS e 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 sono presenti due dispositivi Android (o Android + iGHA) con insiemi diversi di credenziali preferite, avrà la precedenza il dispositivo che ha configurato originariamente il TBR.

Onboarding di TBR di terze parti

Al momento, lo spazio di archiviazione della credenziale non è limitato all'ecosistema per la smart home dell'utente (Struttura). Ogni dispositivo Android avrà il proprio spazio di archiviazione BAID, ma quando nella rete è presente un TBR di Google, gli altri dispositivi Android e iOS con l'app Google Home per iOS si sincronizzeranno con questo TBR e tenteranno di impostare le credenziali locali sullo spazio di archiviazione dello smartphone.

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

  • Se esiste una rete preferita, il fornitore deve utilizzarla. In questo modo, se possibile, i dispositivi Thread sono connessi a una singola rete Thread.
  • Se 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 potranno migliorare la copertura e la robustezza della tua rete mesh con dispositivi aggiuntivi

cd8bc726f67b1fa1.png

4. Clonare e modificare l'app per Android

Abbiamo creato un'app per Android che mostra le principali chiamate possibili all'API Thread. Puoi utilizzare questi pattern nella tua app. In questo codelab cloneremo l'app di esempio 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 binari precompilati 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 vai al repository clonato.
  3. Attiva la modalità sviluppatore sullo 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> (Windows, Linux)
  6. Vai a Ruota -> Utilità per sviluppatori -> Rete Thread
  7. Interagisci con le diverse opzioni disponibili. Nelle sezioni seguenti analizzeremo il codice eseguito su ogni pulsante.

Esistono credenziali preferite?

La prima domanda che un produttore di dispositivi TBR deve porre a Google è se nel dispositivo esiste già un insieme di credenziali preferite. Questo dovrebbe essere il punto di partenza del flusso. Il codice seguente esegue query su GPS sull'esistenza delle credenziali. Non viene richiesto il consenso dell'utente perché non vengono condivise credenziali.

/**
* 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)
 }
}

Ottenere le credenziali preferite del GPS

Se esistono, devi leggere le credenziali. L'unica differenza rispetto al codice precedente è che, dopo aver ricevuto intentSenderResult, vuoi creare e lanciare un'intenzione utilizzando il risultato del mittente.

Nel nostro codice, per motivi di organizzazione/architettura utilizziamo un MutableLiveData<IntentSender?> perché il codice originale si trova nel ViewModel (ThreadViewModel.kt) e gli osservatori di intent si trovano nel frammento dell'attività (ThreadFragment.kt). Pertanto, una volta che intentSenderResult viene pubblicato 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()
 }
}

Verrà attivato il consenso dell'utente 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

Esistano 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 è il primo a essere registrato, queste credenziali verranno copiate nell'insieme di credenziali preferite. Queste informazioni sono accessibili a qualsiasi app sullo smartphone, a condizione che l'utente lo autorizzi.

/**
* 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)
     }
 }
}

Impostazione delle credenziali per il prodotto TBR

Questa parte è di proprietà di ciascun fornitore e in questo codelab la implementiamo tramite il server REST HTTP DBUS + Python o il server REST HTTP 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 di Python di esempio.

Creazione e importazioni

Quando crei la tua app per Android, devi apportare modifiche al manifest, alla compilazione e alle importazioni per supportare il Modulo Thread di Google Play Services. I tre snippet seguenti riepilogano la maggior parte delle aggiunte.

Tieni presente che la nostra app di esempio è progettata principalmente per la messa in servizio di Matter. Pertanto, i file Manifest e Gradle sono più complessi delle aggiunte necessarie per utilizzare solo le credenziali Thread.

Modifiche al 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.

Questo è molto utile quando inserisci le informazioni del tuo TBR nello spazio di archiviazione della credenziale GPS. Tuttavia, il suo utilizzo non rientra nell'ambito di questo codelab. Utilizziamo la libreria di rilevamento dei servizi Android 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 RPi OTBR. La nostra app di esempio mostra 8 pulsanti:

91979bf065e9673d.png

Una possibile sequenza per l'onboarding del tuo TBR è:

  1. Esegui una query per verificare se esistono credenziali preferenziali (blu, 1ª riga)
  2. A seconda della risposta
  3. Ottieni le credenziali preferite per il GPS (blu, 2ª riga)
  4. Imposta le credenziali TBR in GPS (blu, 3a riga) -> Seleziona il TBR -> Crea casuale -> Inserisci il nome della rete -> Ok
  5. Ora che hai le credenziali preferite, impostale nell'OTBR utilizzando Imposta le credenziali RPi OTBR, che le applicherà all'insieme in attesa.

Il valore predefinito per l'app di esempio è un ritardo di 10 secondi. Pertanto, al termine di questo periodo, le credenziali del tuo RPi TBR (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 Android di esempio e analizzato diversi snippet di codice che utilizzano le API Thread Storage di Google Play Services. Abbiamo utilizzato queste API per avere un set di dati comune che possiamo integrare in un TBR RPi, che mostra il TBR di un fornitore.

Avere tutti i dispositivi TBR di un utente nella stessa rete migliora la resilienza e la copertura della rete Thread. Inoltre, impedisce percorsi dell'utente errati 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 Thread Border Router.

8. Riferimenti

Co-processore RCP

DBUS