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 (su collegamento Wi-Fi/Ethernet) e SRP (su rete Thread).

Questo codelab si basa su quello precedente e spiega come 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 che esegue Open Thread Border Router (OTBR)
  • Scheda che fornisce la connettività IEEE 802.15.4 come Radio Co-Processor (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 blocco di base 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 localmente con l'agente OTBR tramite DBUS, mentre la seconda sfrutta 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 intorno a uno dei due 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: API DBUS e HTTP su script Python

91e5fdeed83e9354.png

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

Sull'RPi che fungerà da OTBR, installa le dipendenze di 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 sul percorso principale per una richiesta GET per recuperare le credenziali Thread o una richiesta POST per impostare le credenziali Thread. Il payload è sempre una struttura JSON con il TLV.

La seguente richiesta PUT imposta nuove credenziali di thread in attesa su OTBR utilizzando il percorso /node/dataset/pending. In questo caso, le credenziali in attesa vengono applicate in 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 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 al percorso del bus io.openthread.BorderRouter.wpan0, 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:

$ 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

Per impostazione predefinita, OpenThread Border Router viene creato con il flag REST_API=1, che abilita l'API REST. Se la build di un codelab precedente non ha abilitato l'API REST, assicurati di creare OTBR su 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 (documentate qui). Puoi utilizzare il browser, curl o wget per esaminarne i contenuti. Tra i molti 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. Configurazione del framework delle credenziali su Android

Credenziali preferite

Google Play Services su Android consente e prevede la registrazione delle credenziali per tutti i TBR nella tua rete. Ognuno è identificato dal proprio ID agente router di confine (BAID). Per eseguire questa attività, utilizzerai il metodo addCredentials() dell'interfaccia ThreadNetworkClient. Il primo TBR aggiunto allo spazio di archiviazione dei servizi Google Play determina le credenziali preferite per questo dispositivo mobile.

L'app che aggiunge un insieme di credenziali di rete Thread al proprio BAID diventa proprietaria delle credenziali e dispone di tutte le autorizzazioni 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 viene aggiornata la rete del router di confine Thread. Anche se queste informazioni non vengono utilizzate oggi, potremmo fornire percorsi migliorati in futuro.

Anche se il primo TBR viene escluso in un secondo momento, le credenziali preferite rimarranno sul dispositivo Android. Una volta impostate, le 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 non esistono credenziali su Android, i dispositivi le estraggono dai TBR di Google nella tua rete e queste credenziali diventano le credenziali preferite. La sincronizzazione tra i TBR e il dispositivo Android avviene solo se il TBR è accoppiato a un singolo utente o a due utenti che si trovano nella stessa casa smart (struttura).

Questo processo si verifica 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 nell'archivio iOS, se non esistono credenziali preferite.

Se nella stessa rete esistono due dispositivi Android (o Android + iGHA) con diversi set di credenziali preferite, il dispositivo che ha configurato originariamente il TBR avrà la precedenza sul TBR.

Onboarding di TBR di terze parti

Al momento, l'archiviazione delle credenziali non è limitata dalla casa smart (struttura) dell'utente. Ogni dispositivo Android avrà il proprio spazio di archiviazione BAID, ma una volta che nella rete è presente un TBR di Google, gli altri dispositivi Android e iOS che eseguono l'app Google Home per iOS si sincronizzeranno con questo TBR e tenteranno di impostare le credenziali locali nella memoria dello smartphone.

Prima che un nuovo OOB TBR 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, i dispositivi Thread sono connessi a una singola rete Thread, se possibile.
  • 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 portata 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 file binari precompilati per esaminare 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 seleziona il 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 Rotella -> 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 TBR deve porre a Google è se esiste già un insieme preferito di credenziali nel dispositivo. Questo dovrebbe essere il punto di partenza del flusso. Il codice riportato di seguito esegue una query su GPS per verificare l'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)
 }
}

Recupero delle 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 avviare un intent utilizzando il risultato del mittente.

Nel nostro codice, per motivi di organizzazione/architettura utilizziamo un MutableLiveData<IntentSender?> perché il codice originale si trova in ViewModel (ThreadViewModel.kt) e gli osservatori di intent si trovano nel fragment dell'attività (ThreadFragment.kt). Pertanto, una volta che intentSenderResult viene pubblicato nei dati live, eseguiamo 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, viene attivato il consenso dell'utente con la condivisione delle credenziali e, se approvato, i contenuti vengono 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

Indipendentemente dalla loro esistenza, devi registrare i tuoi TBR nei servizi Google Play. La tua app sarà l'unica in grado di leggere le credenziali associate all'ID agente di frontiera del tuo TBR, ma se il tuo TBR è il primo a registrarsi, queste credenziali verranno copiate nel set 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 tuo prodotto TBR

Questa parte è proprietaria di ogni 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()
)

Recuperare le credenziali dal prodotto TBR

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

Build e importazioni

Quando crei la tua app per Android, devi apportare modifiche al manifest, alla build e agli import 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 è pensata principalmente per il provisioning 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 l'individuazione 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 non rientra nell'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

Una volta implementate queste chiamate o utilizzata l'app di esempio, puoi eseguire l'onboarding completo del tuo RPi OTBR. La nostra app di esempio espone 8 pulsanti:

91979bf065e9673d.png

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

  1. Query per verificare se esistono credenziali preferenziali (blu, prima riga)
  2. A seconda della risposta
  3. Ottieni le credenziali preferite del GPS (blu, seconda riga)
  4. Imposta le credenziali TBR in GPS (blu, terza 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 Imposta credenziali OTBR RPi, che le applicherà al set in attesa.

L'app di esempio utilizza un ritardo di 10 secondi per impostazione predefinita. Pertanto, dopo 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 per 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 RPi TBR, che mostra il TBR di un fornitore.

Se tutti i TBR di un utente si trovano nella stessa rete, la resilienza e la copertura della rete Thread migliorano. Inoltre, impedisce percorsi 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 router di confine Thread.

8. Riferimenti

Co-processore RCP

DBUS