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

1. Prima di iniziare

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

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

Questo codelab si basa sul precedente, affrontando il modo in cui 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 solidità 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 rete Thread

Cosa imparerai a fare

  • Come 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 Raspber Pi 4 o un'altra scheda basata su Linux che esegue il router di confine Open Thread (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 componente di base di cui abbiamo bisogno è un'interfaccia che ci consenta di leggere le credenziali attive e di scrivere quelle in sospeso nell'OTBR. Quando crei una 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 sfrutta l'API Rest che può essere creata su OTBR.

Nessuno dei due metodi è sicuro e non deve essere utilizzato così come è in un ambiente di produzione. Tuttavia, un fornitore può creare la crittografia in base a entrambi i metodi per utilizzarla in un ambiente di produzione oppure puoi estendere il tuo servizio di monitoraggio per emettere chiamate HTTP di loopback o di chiamate 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, richiamando infine i comandi DBUS.

Sul 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

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

La seguente richiesta PUT imposta le nuove credenziali del thread in sospeso nell'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 su /node/dataset/active recupera le credenziali attive attualmente.

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à. Ecco 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 indicate qui.

Opzione 2: API HTTP Rest nativa dell'agente OTBR

c748ca5151b6cacb.png

Per impostazione predefinita, il router di confine OpenThread crea il flag REST_API=1, abilitando l'API REST. Nel caso in cui la build di un codelab precedente non avesse abilitato l'API REST, assicurati di creare OTBR sul tuo 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à nella OTBR (documentata qui). Puoi usare il browser, curl o wget, per ispezionarne i contenuti. Tra i numerosi percorsi supportati ci sono i casi d'uso descritti sopra, con il verbo GET in /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 relativo 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 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 il proprietario delle credenziali e ha autorizzazioni complete per accedervi. Se tenti di accedere alle credenziali aggiunte da altre app, riceverai un errore PERMISSION_DENIED. Tuttavia, le credenziali preferite sono sempre disponibili per qualsiasi app, previa autorizzazione dell'utente. Ti consigliamo di mantenere aggiornate le credenziali archiviate in Google Play Services quando la rete del router di confine Thread viene aggiornata. Anche se queste informazioni non vengono utilizzate oggi, potremmo offrire percorsi migliori in futuro.

Anche se il primo TBR viene successivamente escluso, le credenziali preferite rimarranno sul dispositivo Android. Dopo la configurazione, le altre app che gestiscono le credenziali Thread potrebbero ottenere le credenziali da una chiamata getPreferredCredentials().

Google TBR Sync

I dispositivi Android si sincronizzano automaticamente con le TBR di Google. Se non esistono credenziali su Android, i dispositivi le estraggono dai TBR di Google nella tua rete e quelle credenziali diventano le credenziali preferite. La sincronizzazione tra i TBR e il dispositivo Android avviene solo se la TBR è accoppiata a un singolo utente o con due utenti che si trovano nella stessa smart home ( 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 sullo spazio di archiviazione iOS, se non esistono credenziali preferite.

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

onboarding TBR di terze parti

Al momento l'ambito dello spazio di archiviazione delle credenziali non è limitato alla smart home dell'utente ( Struttura). Ogni dispositivo Android avrà il proprio spazio di archiviazione BAID, ma una volta che è presente una TBR Google nella rete, gli altri dispositivi Android e iOS su cui è in esecuzione l'app Google Home per iOS si sincronizzeranno con questo TBR e proveranno a impostare le credenziali locali sullo spazio di archiviazione del telefono.

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, deve essere utilizzata dal fornitore. Ciò garantisce che i dispositivi Thread siano connessi a una singola rete Thread quando 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 quelle standard impostate su tutti i TBR basati su Google e altri fornitori potranno migliorare la copertura e la solidità del tuo mesh con dispositivi aggiuntivi

cd8bc726f67b1fa1.png

4. Clonazione e modifica di un'app 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 Google Home per Matter da GitHub.

Tutto il codice sorgente mostrato qui è già codificato nell'app di esempio. Puoi modificarlo in base alle tue esigenze, ma puoi semplicemente clonare l'app o eseguire i programmi 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 cursore 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 al volante -> Developer Utilities -> Thread Network.
  7. Interagisci con le diverse opzioni disponibili. Nelle sezioni che seguono illustreremo il codice che viene eseguito su ogni pulsante.

Esistono le credenziali preferite?

La prima domanda che un produttore di TBR dovrebbe porre a Google è se nel dispositivo sia già presente un set di credenziali preferito. Questo dovrebbe essere il punto di partenza del tuo flusso. Il codice seguente esegue una query al GPS per verificare l'esistenza di 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 per il GPS

Se esistono, ti consigliamo di leggere le credenziali. L'unica differenza rispetto al codice precedente è che, dopo aver ricevuto il intentSenderResult, vuoi creare e avviare un intent utilizzando il risultato del mittente.

Nel nostro codice, per scopi di organizzazione/architettura utilizziamo un MutableLiveData<IntentSender?> perché il codice originale è nel ViewModel (ThreadViewModel.kt) e gli osservatori dell'intent si trovano nel frammento 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()
 }
}

Questa operazione attiverà il consenso degli utenti con la condivisione delle credenziali e, se approvata, restituirà i contenuti 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, "")
   }
 }

Di seguito è descritta la procedura per pubblicare le credenziali su MutableLiveData<ThreadNetworkCredentials?>.

Impostazione delle credenziali GPS

Che esistano o meno, devi registrare la tua TBR in Google Play Services. La tua app sarà l'unica in grado di leggere le credenziali associate all'ID agente di confine della tua TBR, ma se la tua TBR è la prima a registrarsi, queste credenziali verranno copiate nel set di credenziali preferite. Tali informazioni sono accessibili a qualsiasi App sul telefono, purché l'utente le 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 ogni fornitore e in questo codelab la implementiamo tramite il server resto HTTP DBUS+Python o il server resto 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 TBR. Consulta lo script Python di esempio.

Creazione e importazione

Durante la creazione della tua app per Android, devi apportare modifiche al file manifest, alla build e alle importazioni 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 creata principalmente per la messa in servizio di Matter. Di conseguenza, i relativi file manifest e Gradle sono più complessi delle aggiunte necessarie solo per l'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 di mDNS/SD

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

Ciò è molto utile quando si inseriscono informazioni del TBR nell'archivio 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 Sample, puoi eseguire l'onboarding completo della tua OTBR RPi. La nostra app di esempio mostra 8 pulsanti:

91979bf065e9673d.png

Una possibile sequenza per l'onboarding del TBR è:

  1. Eseguire una query sull'esistenza di credenziali preferenziali (blu, prima riga)
  2. In base alla risposta
  3. Ottieni credenziali preferite per il GPS (blu, seconda riga)
  4. Imposta le credenziali TBR nel 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 l'opzione Set RPi OTBR credentials (Imposta credenziali OTBR RPi) per applicarle al set in attesa.

L'impostazione predefinita per l'app di esempio è di 10 secondi. Dopo questo periodo, quindi, 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 le API Thread Storage di Google Play Services. Abbiamo utilizzato queste API per disporre di un set di dati comune da inserire in un TBR RPi, che mostra il TBR di un fornitore.

Avere tutte le TBR di un utente nella stessa rete migliora la resilienza e la copertura della rete Thread. Inoltre, impedisce percorsi degli utenti errati in cui le app non possono integrare i dispositivi Thread perché non hanno accesso alle credenziali.

Ci auguriamo che questo codelab e queste 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