From 247ca9ef8b59141cfc471a34fb84003e5a015288 Mon Sep 17 00:00:00 2001 From: Milan Lenco Date: Fri, 21 Jul 2023 10:04:49 +0200 Subject: [PATCH] Implement features of the new cellular APIs This is the second part of the commit series implementing the new cellular APIs in EVE. The first part was mostly about extending zedagent microservice to support parsing of the new protobuf messages and extending structures used for delivering config and status between NIM and wwan microservice. But wwan and for the most part also NIM still worked only in the backward-compatible mode. In this commit, we make substantial enhancements to the wwan microservice and some additions to NIM to implement the new cellular APIs in their full scope. Part II changelog: * implemented username/password authentication for cellular networks * added logging with configurable verbose level to wwan microservice * upgraded QMI and MBIM CLI tools to bring in some later added commands needed to implement the new cellular APIs. Note that some of the patches we were applying to the previously used versions are already included in the newer versions and we can therefore remove these patch files * improved robustness of the wwan microservice - decreased reaction time and improved ability to recover from lost/broken connectivity * finalized support for multiple cellular modems (just few changes were needed, most of the code was already there) * more cellular status data are now published, e.g.: time when the connection was established, info about currently used provided, used Radio Access Technology (UMTS / LTE / ...), modem manufacturer, etc. * added support for modems with multiple SIMs in the DSSS mode (single standby) Signed-off-by: Milan Lenco --- pkg/pillar/cipher/cipher.go | 2 + pkg/pillar/dpcreconciler/linux.go | 160 +++++- pkg/pillar/go.mod | 1 + pkg/pillar/go.sum | 3 + pkg/pillar/types/cipherinfotypes.go | 2 + pkg/pillar/types/wwan.go | 17 +- .../Luzifer/go-openssl/v4/.travis.yml | 11 + .../Luzifer/go-openssl/v4/History.md | 45 ++ .../github.com/Luzifer/go-openssl/v4/LICENSE | 202 +++++++ .../Luzifer/go-openssl/v4/README.md | 89 +++ .../github.com/Luzifer/go-openssl/v4/keys.go | 63 ++ .../Luzifer/go-openssl/v4/openssl.go | 274 +++++++++ .../golang.org/x/crypto/pbkdf2/pbkdf2.go | 77 +++ pkg/pillar/vendor/modules.txt | 4 + pkg/wwan/Dockerfile | 8 +- ...tile-for-g_once_init_enter-locations.patch | 96 ---- ...tile-for-g_once_init_enter-locations.patch | 124 ---- pkg/wwan/usr/bin/wwan-init.sh | 542 +++++++++++++++--- pkg/wwan/usr/bin/wwan-loc.sh | 18 +- pkg/wwan/usr/bin/wwan-mbim.sh | 385 ++++++++++--- pkg/wwan/usr/bin/wwan-qmi.sh | 436 +++++++++++--- 21 files changed, 2051 insertions(+), 508 deletions(-) create mode 100644 pkg/pillar/vendor/github.com/Luzifer/go-openssl/v4/.travis.yml create mode 100644 pkg/pillar/vendor/github.com/Luzifer/go-openssl/v4/History.md create mode 100644 pkg/pillar/vendor/github.com/Luzifer/go-openssl/v4/LICENSE create mode 100644 pkg/pillar/vendor/github.com/Luzifer/go-openssl/v4/README.md create mode 100644 pkg/pillar/vendor/github.com/Luzifer/go-openssl/v4/keys.go create mode 100644 pkg/pillar/vendor/github.com/Luzifer/go-openssl/v4/openssl.go create mode 100644 pkg/pillar/vendor/golang.org/x/crypto/pbkdf2/pbkdf2.go delete mode 100644 pkg/wwan/patches/libmbim/0002-core-drop-volatile-for-g_once_init_enter-locations.patch delete mode 100644 pkg/wwan/patches/libqmi/0001-core-drop-volatile-for-g_once_init_enter-locations.patch diff --git a/pkg/pillar/cipher/cipher.go b/pkg/pillar/cipher/cipher.go index 71743431632..3a1556d86f4 100644 --- a/pkg/pillar/cipher/cipher.go +++ b/pkg/pillar/cipher/cipher.go @@ -24,6 +24,8 @@ func getEncryptionBlock( decBlock.DsPassword = zconfigDecBlockPtr.DsPassword decBlock.WifiUserName = zconfigDecBlockPtr.WifiUserName decBlock.WifiPassword = zconfigDecBlockPtr.WifiPassword + decBlock.CellNetUsername = zconfigDecBlockPtr.CellularNetUsername + decBlock.CellNetPassword = zconfigDecBlockPtr.CellularNetPassword decBlock.ProtectedUserData = zconfigDecBlockPtr.ProtectedUserData return decBlock } diff --git a/pkg/pillar/dpcreconciler/linux.go b/pkg/pillar/dpcreconciler/linux.go index 8cc3728a716..463441efe10 100644 --- a/pkg/pillar/dpcreconciler/linux.go +++ b/pkg/pillar/dpcreconciler/linux.go @@ -5,26 +5,27 @@ import ( "errors" "fmt" "net" + "os" "strconv" "strings" "sync" "syscall" "time" + "github.com/Luzifer/go-openssl/v4" dg "github.com/lf-edge/eve/libs/depgraph" "github.com/lf-edge/eve/libs/reconciler" "github.com/lf-edge/eve/pkg/pillar/base" "github.com/lf-edge/eve/pkg/pillar/cipher" "github.com/lf-edge/eve/pkg/pillar/devicenetwork" - "github.com/lf-edge/eve/pkg/pillar/iptables" - "github.com/lf-edge/eve/pkg/pillar/pubsub" - "github.com/lf-edge/eve/pkg/pillar/types" - "github.com/vishvananda/netlink" - generic "github.com/lf-edge/eve/pkg/pillar/dpcreconciler/genericitems" linux "github.com/lf-edge/eve/pkg/pillar/dpcreconciler/linuxitems" + "github.com/lf-edge/eve/pkg/pillar/iptables" "github.com/lf-edge/eve/pkg/pillar/netmonitor" + "github.com/lf-edge/eve/pkg/pillar/pubsub" + "github.com/lf-edge/eve/pkg/pillar/types" fileutils "github.com/lf-edge/eve/pkg/pillar/utils/file" + "github.com/vishvananda/netlink" ) // Device connectivity configuration is modeled using dependency graph (see libs/depgraph). @@ -147,6 +148,13 @@ const ( kubeCNIBridge = "cni0" ) +const ( + // File with random ID that is regenerated by the kernel on each boot. + // Used as a passphrase to encrypt cellular network password before publishing + // from NIM to the wwan microservice. + bootIDFile = "/proc/sys/kernel/random/boot_id" +) + // LinuxDpcReconciler is a DPC-reconciler for Linux network stack, // i.e. it configures and uses Linux networking to provide device connectivity. type LinuxDpcReconciler struct { @@ -183,11 +191,11 @@ type LinuxDpcReconciler struct { resumeReconcile chan struct{} resumeAsync <-chan string // nil if no async ops - prevArgs Args - prevStatus ReconcileStatus - radioSilence types.RadioSilence - - HVTypeKube bool + prevArgs Args + prevStatus ReconcileStatus + radioSilence types.RadioSilence + HVTypeKube bool + encCellularPasswords map[string]string } type pendingReconcile struct { @@ -236,6 +244,7 @@ func (r *LinuxDpcReconciler) init() (startWatcher func()) { configurator := registry.GetConfigurator(generic.Wwan{}) r.wwanConfigurator = configurator.(*generic.WwanConfigurator) r.watcherControl = make(chan watcherCtrl, 10) + r.encCellularPasswords = make(map[string]string) netEvents := r.NetworkMonitor.WatchEvents( context.Background(), "linux-dpc-reconciler") go r.watcher(netEvents) @@ -371,6 +380,7 @@ func (r *LinuxDpcReconciler) Reconcile(ctx context.Context, args Args) Reconcile } if r.gcpChanged(args.GCP) { r.addPendingReconcile(ACLsSG, "GCP change", false) + r.addPendingReconcile(WirelessSG, "GCP change", false) } if r.aaChanged(args.AA) { changed := r.updateCurrentPhysicalIO(args.DPC, args.AA) @@ -420,7 +430,7 @@ func (r *LinuxDpcReconciler) Reconcile(ctx context.Context, args Args) Reconcile case L3SG: intSG = r.getIntendedL3Cfg(args.DPC) case WirelessSG: - intSG = r.getIntendedWirelessCfg(args.DPC, args.AA, args.RS) + intSG = r.getIntendedWirelessCfg(args.DPC, args.AA, args.RS, args.GCP) case ACLsSG: intSG = r.getIntendedACLs(args.DPC, args.GCP) default: @@ -615,6 +625,11 @@ func (r *LinuxDpcReconciler) gcpChanged(newGCP types.ConfigItemValueMap) bool { if prevAllowVNC != newAllowVNC { return true } + prevWwanLogLevel := r.prevArgs.GCP.AgentSettingStringValue("wwan", types.LogLevel) + newWwanLogLevel := newGCP.AgentSettingStringValue("wwan", types.LogLevel) + if prevWwanLogLevel != newWwanLogLevel { + return true + } return false } @@ -815,7 +830,7 @@ func (r *LinuxDpcReconciler) updateIntendedState(args Args) { r.intendedState.PutSubGraph(r.getIntendedPhysicalIO(args.DPC)) r.intendedState.PutSubGraph(r.getIntendedLogicalIO(args.DPC)) r.intendedState.PutSubGraph(r.getIntendedL3Cfg(args.DPC)) - r.intendedState.PutSubGraph(r.getIntendedWirelessCfg(args.DPC, args.AA, args.RS)) + r.intendedState.PutSubGraph(r.getIntendedWirelessCfg(args.DPC, args.AA, args.RS, args.GCP)) r.intendedState.PutSubGraph(r.getIntendedACLs(args.DPC, args.GCP)) } @@ -1226,7 +1241,8 @@ func (r *LinuxDpcReconciler) getIntendedArps(dpc types.DevicePortConfig) dg.Grap } func (r *LinuxDpcReconciler) getIntendedWirelessCfg(dpc types.DevicePortConfig, - aa types.AssignableAdapters, radioSilence types.RadioSilence) dg.Graph { + aa types.AssignableAdapters, radioSilence types.RadioSilence, + gcp types.ConfigItemValueMap) dg.Graph { graphArgs := dg.InitArgs{ Name: WirelessSG, Description: "Configuration for wireless connectivity", @@ -1244,7 +1260,7 @@ func (r *LinuxDpcReconciler) getIntendedWirelessCfg(dpc types.DevicePortConfig, // because when the DPC arrives, wwan microservice won't be blocked on retrieving // some state data but will be ready to apply the config immediately. intendedWirelessCfg.PutItem( - r.getIntendedWwanConfig(dpc, aa, rsImposed), nil) + r.getIntendedWwanConfig(dpc, aa, rsImposed, gcp), nil) } return intendedWirelessCfg } @@ -1333,11 +1349,14 @@ func (r *LinuxDpcReconciler) getWifiCredentials(wifi types.WifiConfig) (types.En } func (r *LinuxDpcReconciler) getIntendedWwanConfig(dpc types.DevicePortConfig, - aa types.AssignableAdapters, radioSilence bool) dg.Item { + aa types.AssignableAdapters, radioSilence bool, gcp types.ConfigItemValueMap) dg.Item { + logLevel := gcp.AgentSettingStringValue("wwan", types.LogLevel) config := types.WwanConfig{ RadioSilence: radioSilence, + Verbose: logLevel == "debug" || logLevel == "trace", Networks: []types.WwanNetworkConfig{}, } + for _, port := range dpc.Ports { if port.WirelessCfg.WType != types.WirelessTypeCellular { continue @@ -1370,6 +1389,26 @@ func (r *LinuxDpcReconciler) getIntendedWwanConfig(dpc types.DevicePortConfig, "skipping", port.Logicallabel) continue } + decBlock, err := r.getWwanCredentials(accessPoint) + if err != nil { + r.Log.Error(err) + r.Log.Warnf("getIntendedWwanConfig: failed to get credentials for port %s, "+ + "skipping", port.Logicallabel) + continue + } + + var encPassword string + if decBlock.CellNetUsername != "" { + encPassword, err = r.encryptCellularPassword(port.Logicallabel, + decBlock.CellNetPassword) + if err != nil { + r.Log.Error(err) + r.Log.Warnf( + "getIntendedWwanConfig: failed to encrypt user password for port %s, "+ + "skipping", port.Logicallabel) + continue + } + } // Prefer USB and PCI addresses over interface name. // Plus we want to avoid changing /run/wwan/config.json just to put there // discovered interface name (it is the wwan microservice that discovered it anyway @@ -1383,23 +1422,92 @@ func (r *LinuxDpcReconciler) getIntendedWwanConfig(dpc types.DevicePortConfig, physAddress.Interface = port.IfName } network := types.WwanNetworkConfig{ - // TODO: Username + Password (will be done in the next PR) - LogicalLabel: port.Logicallabel, - PhysAddrs: physAddress, - SIMSlot: accessPoint.SIMSlot, - APN: accessPoint.APN, - PreferredPLMNs: accessPoint.PreferredPLMNs, - PreferredRATs: accessPoint.PreferredRATs, - ForbidRoaming: accessPoint.ForbidRoaming, - Proxies: port.Proxies, - Probe: port.WirelessCfg.Cellular.Probe, - LocationTracking: port.WirelessCfg.Cellular.LocationTracking, + LogicalLabel: port.Logicallabel, + PhysAddrs: physAddress, + SIMSlot: accessPoint.SIMSlot, + APN: accessPoint.APN, + AuthProtocol: accessPoint.AuthProtocol, + Username: decBlock.CellNetUsername, + EncryptedPassword: encPassword, + PreferredPLMNs: accessPoint.PreferredPLMNs, + PreferredRATs: accessPoint.PreferredRATs, + ForbidRoaming: accessPoint.ForbidRoaming, + Proxies: port.Proxies, + Probe: port.WirelessCfg.Cellular.Probe, + LocationTracking: port.WirelessCfg.Cellular.LocationTracking, } config.Networks = append(config.Networks, network) } return generic.Wwan{Config: config} } +func (r *LinuxDpcReconciler) getWwanCredentials(ap *types.CellularAccessPoint) ( + decBlock types.EncryptionBlock, err error) { + if !ap.IsCipher { + return decBlock, nil + } + decryptAvailable := r.SubControllerCert != nil && r.SubEdgeNodeCert != nil + if !decryptAvailable { + r.CipherMetrics.RecordFailure(r.Log, types.NotReady) + return decBlock, fmt.Errorf( + "missing certificates for decryption of cellular network credentials") + } + status, decBlock, err := cipher.GetCipherCredentials( + &cipher.DecryptCipherContext{ + Log: r.Log, + AgentName: r.AgentName, + AgentMetrics: r.CipherMetrics, + SubControllerCert: r.SubControllerCert, + SubEdgeNodeCert: r.SubEdgeNodeCert, + }, + ap.CipherBlockStatus) + if r.PubCipherBlockStatus != nil { + r.PubCipherBlockStatus.Publish(status.Key(), status) + } + if err != nil { + r.CipherMetrics.RecordFailure(r.Log, types.DecryptFailed) + return decBlock, fmt.Errorf( + "failed to decrypt cellular network credentials: %v", err) + } + return decBlock, nil +} + +// Encrypt user password using AES-256-CBC with the key derived by the PBKDF2 +// method, taking kernel-generated /proc/sys/kernel/random/boot_id as the input. +// Note that even though the config with the password is passed from NIM to the wwan +// microservice using the *in-memory only* /run filesystem, we still encrypt +// the password to avoid accidental exposure when the content of /run/wwan +// is dumped as part of a customer issue report. +func (r *LinuxDpcReconciler) encryptCellularPassword(portLL, password string) ( + ciphertext string, err error) { + // Key includes port logical label so that even if user uses the same password + // for multiple APNs, the encrypted passwords are different for more secrecy. + key := portLL + "/" + password + ciphertext, alreadyEncrypted := r.encCellularPasswords[key] + if alreadyEncrypted { + // EncryptBytes uses random salt meaning that it will return different + // ciphertext on each run even for the same password. + // However, we do not want to change generated /run/wwan/config.json unless + // there is an actual config change (triggers many operations in the wwan + // microservice), so we remember already encrypted passwords. + return ciphertext, nil + } + bootID, err := os.ReadFile(bootIDFile) + if err != nil { + return "", err + } + passphrase := strings.TrimSpace(string(bootID)) + openSSL := openssl.New() + encBytes, err := openSSL.EncryptBytes( + passphrase, []byte(password), openssl.PBKDF2SHA256) + if err != nil { + return "", err + } + ciphertext = string(encBytes) + r.encCellularPasswords[key] = ciphertext + return ciphertext, nil +} + func (r *LinuxDpcReconciler) getIntendedACLs( dpc types.DevicePortConfig, gcp types.ConfigItemValueMap) dg.Graph { graphArgs := dg.InitArgs{ diff --git a/pkg/pillar/go.mod b/pkg/pillar/go.mod index 7f5e48b5136..15a74daa98b 100644 --- a/pkg/pillar/go.mod +++ b/pkg/pillar/go.mod @@ -3,6 +3,7 @@ module github.com/lf-edge/eve/pkg/pillar go 1.20 require ( + github.com/Luzifer/go-openssl/v4 v4.1.0 github.com/anatol/smart.go v0.0.0-20220615232124-371056cd18c3 github.com/bicomsystems/go-libzfs v0.4.0 github.com/containerd/cgroups v1.0.4 diff --git a/pkg/pillar/go.sum b/pkg/pillar/go.sum index 39e7e8b9f4a..eb4038381ed 100644 --- a/pkg/pillar/go.sum +++ b/pkg/pillar/go.sum @@ -157,6 +157,8 @@ github.com/Djarvur/go-err113 v0.0.0-20200410182137-af658d038157/go.mod h1:4UJr5H github.com/Djarvur/go-err113 v0.1.0/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs= github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20191009163259-e802c2cb94ae/go.mod h1:mjwGPas4yKduTyubHvD1Atl9r1rUq8DfVy+gkVvZ+oo= github.com/GoogleCloudPlatform/k8s-cloud-provider v0.0.0-20190822182118-27a4ced34534/go.mod h1:iroGtC8B3tQiqtds1l+mgk/BBOrxbqjH+eUfFQYRc14= +github.com/Luzifer/go-openssl/v4 v4.1.0 h1:8qi3Z6f8Aflwub/Cs4FVSmKUEg/lC8GlODbR2TyZ+nM= +github.com/Luzifer/go-openssl/v4 v4.1.0/go.mod h1:3i1T3Pe6eQK19d86WhuQzjLyMwBaNmGmt3ZceWpWVa4= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/semver/v3 v3.0.3/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Masterminds/semver/v3 v3.1.0/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= @@ -1770,6 +1772,7 @@ golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200422194213-44a606286825/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= diff --git a/pkg/pillar/types/cipherinfotypes.go b/pkg/pillar/types/cipherinfotypes.go index 8ff10dd1dda..9ca26017da6 100644 --- a/pkg/pillar/types/cipherinfotypes.go +++ b/pkg/pillar/types/cipherinfotypes.go @@ -143,5 +143,7 @@ type EncryptionBlock struct { DsPassword string WifiUserName string // If the authentication type is EAP WifiPassword string + CellNetUsername string + CellNetPassword string ProtectedUserData string } diff --git a/pkg/pillar/types/wwan.go b/pkg/pillar/types/wwan.go index 51332090c24..29e07c11ee8 100644 --- a/pkg/pillar/types/wwan.go +++ b/pkg/pillar/types/wwan.go @@ -37,7 +37,6 @@ func (wc WwanConfig) Equal(wc2 WwanConfig) bool { // WwanNetworkConfig contains configuration for a single cellular network. // In case there are multiple SIM cards/slots in the modem, WwanNetworkConfig // contains config only for the activated one. -// TODO: Add username + password (will be done in the next PR) type WwanNetworkConfig struct { // Logical label in PhysicalIO. LogicalLabel string `json:"logical-label"` @@ -51,6 +50,17 @@ type WwanNetworkConfig struct { // Access Point Network to connect into. // By default, it is "internet". APN string `json:"apn"` + // Some cellular networks require authentication. + AuthProtocol WwanAuthProtocol `json:"auth-protocol"` + Username string `json:"username,omitempty"` + // User password (if provided) is encrypted using AES-256-CBC with key derived + // by the PBKDF2 method, taking kernel-generated /proc/sys/kernel/random/boot_id + // as the input. + // Note that even though the config with the password is passed from NIM to the wwan + // microservice using the *in-memory only* /run filesystem, we still encrypt the password + // to avoid accidental exposure when the content of /run/wwan is dumped as part + // of a customer issue report. + EncryptedPassword string `json:"encrypted-password,omitempty"` // The set of cellular network operators that modem should preferably try to register // and connect into. // Network operator should be referenced by PLMN (Public Land Mobile Network) code, @@ -127,6 +137,11 @@ func (wnc WwanNetworkConfig) Equal(wnc2 WwanNetworkConfig) bool { wnc.APN != wnc2.APN { return false } + if wnc.AuthProtocol != wnc2.AuthProtocol || + wnc.Username != wnc2.Username || + wnc.EncryptedPassword != wnc2.EncryptedPassword { + return false + } if !generics.EqualLists(wnc.PreferredPLMNs, wnc2.PreferredPLMNs) || !generics.EqualLists(wnc.PreferredRATs, wnc2.PreferredRATs) || wnc.ForbidRoaming != wnc2.ForbidRoaming { diff --git a/pkg/pillar/vendor/github.com/Luzifer/go-openssl/v4/.travis.yml b/pkg/pillar/vendor/github.com/Luzifer/go-openssl/v4/.travis.yml new file mode 100644 index 00000000000..cf3e1cb804b --- /dev/null +++ b/pkg/pillar/vendor/github.com/Luzifer/go-openssl/v4/.travis.yml @@ -0,0 +1,11 @@ +dist: bionic +language: go + +go: + - 1.13.x + - 1.14.x + - tip + +script: + - go vet + - go test -v -bench . -cover diff --git a/pkg/pillar/vendor/github.com/Luzifer/go-openssl/v4/History.md b/pkg/pillar/vendor/github.com/Luzifer/go-openssl/v4/History.md new file mode 100644 index 00000000000..f4093381a85 --- /dev/null +++ b/pkg/pillar/vendor/github.com/Luzifer/go-openssl/v4/History.md @@ -0,0 +1,45 @@ +# 4.1.0 / 2020-06-13 + + * Add pre-defined generators and compatibility tests for SHA384 and SHA512 + +# 4.0.0 / 2020-06-13 + + * Breaking: Implement PBKFD2 key derivation (#18) + +# 3.1.0 / 2019-04-29 + + * Add encrypt/decrypt without base64 encoding (thanks [@mcgillowen](https://github.com/mcgillowen)) + * Test: Drop support for pre-1.10, add 1.12 + * Test: Simplify / cleanup test file + +# 3.0.1 / 2019-01-29 + + * Fix: v3 versions require another go-modules name + +# 3.0.0 / 2018-11-02 + + * Breaking: Fix race condition with guessing messagedigest + +# 2.0.2 / 2018-09-18 + + * Fix: v2 versions require another go-modules name + +# 2.0.1 / 2018-09-18 + + * Add modules file + * Fix some linter warnings + * Add benchmarks + +# 2.0.0 / 2018-09-11 + + * Make digest function configurable on encrypt, add tests + * message digest support sha1 and sha256 (thanks @yoozoo) + +# 1.2.0 / 2018-04-26 + + * Add byte-operations, remove import path comment + +# 1.1.0 / 2017-09-18 + + * Add salt validation and improve comments + * Added ability to pass custom salt to everyencrypt call (thanks @VojtechBartos) diff --git a/pkg/pillar/vendor/github.com/Luzifer/go-openssl/v4/LICENSE b/pkg/pillar/vendor/github.com/Luzifer/go-openssl/v4/LICENSE new file mode 100644 index 00000000000..9d74c44fb4b --- /dev/null +++ b/pkg/pillar/vendor/github.com/Luzifer/go-openssl/v4/LICENSE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2015- Knut Ahlers + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/pkg/pillar/vendor/github.com/Luzifer/go-openssl/v4/README.md b/pkg/pillar/vendor/github.com/Luzifer/go-openssl/v4/README.md new file mode 100644 index 00000000000..7314e43a5fd --- /dev/null +++ b/pkg/pillar/vendor/github.com/Luzifer/go-openssl/v4/README.md @@ -0,0 +1,89 @@ +[![](https://badges.fyi/static/godoc/reference/5272B4)](https://pkg.go.dev/github.com/Luzifer/go-openssl/v4) +[![Go Report Card](https://goreportcard.com/badge/github.com/Luzifer/go-openssl)](https://goreportcard.com/report/github.com/Luzifer/go-openssl) +![](https://badges.fyi/github/license/Luzifer/go-openssl) +![](https://badges.fyi/github/latest-tag/Luzifer/go-openssl) +[![](https://travis-ci.org/Luzifer/go-openssl.svg?branch=master)](https://travis-ci.org/Luzifer/go-openssl) + +# Luzifer / go-openssl + +`go-openssl` is a small library wrapping the `crypto/aes` functions in a way the output is compatible to OpenSSL / CryptoJS. For all encryption / decryption processes AES256 is used so this library will not be able to decrypt messages generated with other than `openssl aes-256-cbc`. If you're using CryptoJS to process the data you also need to use AES256 on that side. + +## Version support + +For this library only the latest major version is supported. All prior major versions should no longer be used. + +The versioning is following [SemVer](https://semver.org/) which means upgrading to a newer major version will break your code! + +## OpenSSL compatibility + +### 1.1.0c + +Starting with `v2.0.0` `go-openssl` generates the encryption keys using `sha256sum` algorithm. This is the default introduced in OpenSSL 1.1.0c. When encrypting data you can choose which digest method to use and therefore also continue to use `md5sum`. When decrypting OpenSSL encrypted data `md5sum`, `sha1sum` and `sha256sum` are supported. + +### 1.1.1 + +Starting with `v4.0.0` `go-openssl` is capable of using the PBKDF2 key derivation method for encryption. You can choose to use it by passing the corresponding `CredsGenerator`. + +## Installation + +```bash +# Get the latest version +go get github.com/Luzifer/go-openssl + +# OR get a specific version +go get gopkg.in/Luzifer/go-openssl.v4 +``` + +## Usage example + +The usage is quite simple as you don't need any special knowledge about OpenSSL and/or AES256: + +### Encrypt + +```go +import ( + "fmt" + openssl "gopkg.in/Luzifer/go-openssl.v4" +) + +func main() { + plaintext := "Hello World!" + passphrase := "z4yH36a6zerhfE5427ZV" + + o := openssl.New() + + enc, err := o.EncryptBytes(passphrase, []byte(plaintext), PBKDF2SHA256) + if err != nil { + fmt.Printf("An error occurred: %s\n", err) + } + + fmt.Printf("Encrypted text: %s\n", string(enc)) +} +``` + +### Decrypt + +```go +import ( + "fmt" + openssl "gopkg.in/Luzifer/go-openssl.v4" +) + +func main() { + opensslEncrypted := "U2FsdGVkX19ZM5qQJGe/d5A/4pccgH+arBGTp+QnWPU=" + passphrase := "z4yH36a6zerhfE5427ZV" + + o := openssl.New() + + dec, err := o.DecryptBytes(passphrase, []byte(opensslEncrypted), BytesToKeyMD5) + if err != nil { + fmt.Printf("An error occurred: %s\n", err) + } + + fmt.Printf("Decrypted text: %s\n", string(dec)) +} +``` + +## Testing + +To execute the tests for this library you need to be on a system having `/bin/bash` and `openssl` available as the compatibility of the output is tested directly against the `openssl` binary. The library itself should be usable on all operating systems supported by Go and `crypto/aes`. diff --git a/pkg/pillar/vendor/github.com/Luzifer/go-openssl/v4/keys.go b/pkg/pillar/vendor/github.com/Luzifer/go-openssl/v4/keys.go new file mode 100644 index 00000000000..84c7fada30d --- /dev/null +++ b/pkg/pillar/vendor/github.com/Luzifer/go-openssl/v4/keys.go @@ -0,0 +1,63 @@ +package openssl + +import ( + "crypto/md5" + "crypto/sha1" + "crypto/sha256" + "crypto/sha512" + "hash" + + "golang.org/x/crypto/pbkdf2" +) + +const DefaultPBKDF2Iterations = 10000 + +// CredsGenerator are functions to derive a key and iv from a password and a salt +type CredsGenerator func(password, salt []byte) (Creds, error) + +var ( + BytesToKeyMD5 = NewBytesToKeyGenerator(md5.New) + BytesToKeySHA1 = NewBytesToKeyGenerator(sha1.New) + BytesToKeySHA256 = NewBytesToKeyGenerator(sha256.New) + BytesToKeySHA384 = NewBytesToKeyGenerator(sha512.New384) + BytesToKeySHA512 = NewBytesToKeyGenerator(sha512.New) + PBKDF2MD5 = NewPBKDF2Generator(md5.New, DefaultPBKDF2Iterations) + PBKDF2SHA1 = NewPBKDF2Generator(sha1.New, DefaultPBKDF2Iterations) + PBKDF2SHA256 = NewPBKDF2Generator(sha256.New, DefaultPBKDF2Iterations) + PBKDF2SHA384 = NewPBKDF2Generator(sha512.New384, DefaultPBKDF2Iterations) + PBKDF2SHA512 = NewPBKDF2Generator(sha512.New, DefaultPBKDF2Iterations) +) + +// openSSLEvpBytesToKey follows the OpenSSL (undocumented?) convention for extracting the key and IV from passphrase. +// It uses the EVP_BytesToKey() method which is basically: +// D_i = HASH^count(D_(i-1) || password || salt) where || denotes concatentaion, until there are sufficient bytes available +// 48 bytes since we're expecting to handle AES-256, 32bytes for a key and 16bytes for the IV +func NewBytesToKeyGenerator(hashFunc func() hash.Hash) CredsGenerator { + df := func(in []byte) []byte { + h := hashFunc() + h.Write(in) + return h.Sum(nil) + } + + return func(password, salt []byte) (Creds, error) { + var m []byte + prev := []byte{} + for len(m) < 48 { + a := make([]byte, len(prev)+len(password)+len(salt)) + copy(a, prev) + copy(a[len(prev):], password) + copy(a[len(prev)+len(password):], salt) + + prev = df(a) + m = append(m, prev...) + } + return Creds{Key: m[:32], IV: m[32:48]}, nil + } +} + +func NewPBKDF2Generator(hashFunc func() hash.Hash, iterations int) CredsGenerator { + return func(password, salt []byte) (Creds, error) { + m := pbkdf2.Key(password, salt, iterations, 32+16, hashFunc) + return Creds{Key: m[:32], IV: m[32:48]}, nil + } +} diff --git a/pkg/pillar/vendor/github.com/Luzifer/go-openssl/v4/openssl.go b/pkg/pillar/vendor/github.com/Luzifer/go-openssl/v4/openssl.go new file mode 100644 index 00000000000..0bb188a965d --- /dev/null +++ b/pkg/pillar/vendor/github.com/Luzifer/go-openssl/v4/openssl.go @@ -0,0 +1,274 @@ +package openssl + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "errors" + "fmt" + "io" +) + +// ErrInvalidSalt is returned when a salt with a length of != 8 byte is passed +var ErrInvalidSalt = errors.New("Salt needs to have exactly 8 byte") + +// OpenSSL is a helper to generate OpenSSL compatible encryption +// with autmatic IV derivation and storage. As long as the key is known all +// data can also get decrypted using OpenSSL CLI. +// Code from http://dequeue.blogspot.de/2014/11/decrypting-something-encrypted-with.html +type OpenSSL struct { + openSSLSaltHeader string +} + +// Creds holds a key and an IV for encryption methods +type Creds struct { + Key []byte + IV []byte +} + +func (o Creds) equals(i Creds) bool { + // If lengths does not match no chance they are equal + if len(o.Key) != len(i.Key) || len(o.IV) != len(i.IV) { + return false + } + + // Compare keys + for j := 0; j < len(o.Key); j++ { + if o.Key[j] != i.Key[j] { + return false + } + } + + // Compare IV + for j := 0; j < len(o.IV); j++ { + if o.IV[j] != i.IV[j] { + return false + } + } + + return true +} + +// New instanciates and initializes a new OpenSSL encrypter +func New() *OpenSSL { + return &OpenSSL{ + openSSLSaltHeader: "Salted__", // OpenSSL salt is always this string + 8 bytes of actual salt + } +} + +// DecryptBytes takes a slice of bytes with base64 encoded, encrypted data to decrypt +// and a key-derivation function. The key-derivation function must match the function +// used to encrypt the data. (In OpenSSL the value of the `-md` parameter.) +// +// You should not just try to loop the digest functions as this will cause a race +// condition and you will not be able to decrypt your data properly. +func (o OpenSSL) DecryptBytes(passphrase string, encryptedBase64Data []byte, cg CredsGenerator) ([]byte, error) { + data := make([]byte, base64.StdEncoding.DecodedLen(len(encryptedBase64Data))) + n, err := base64.StdEncoding.Decode(data, encryptedBase64Data) + if err != nil { + return nil, fmt.Errorf("Could not decode data: %s", err) + } + + // Truncate to real message length + data = data[0:n] + + decrypted, err := o.DecryptBinaryBytes(passphrase, data, cg) + if err != nil { + return nil, err + } + return decrypted, nil +} + +// DecryptBinaryBytes takes a slice of binary bytes, encrypted data to decrypt +// and a key-derivation function. The key-derivation function must match the function +// used to encrypt the data. (In OpenSSL the value of the `-md` parameter.) +// +// You should not just try to loop the digest functions as this will cause a race +// condition and you will not be able to decrypt your data properly. +func (o OpenSSL) DecryptBinaryBytes(passphrase string, encryptedData []byte, cg CredsGenerator) ([]byte, error) { + if len(encryptedData) < aes.BlockSize { + return nil, fmt.Errorf("Data is too short") + } + saltHeader := encryptedData[:aes.BlockSize] + if string(saltHeader[:8]) != o.openSSLSaltHeader { + return nil, fmt.Errorf("Does not appear to have been encrypted with OpenSSL, salt header missing") + } + salt := saltHeader[8:] + + creds, err := cg([]byte(passphrase), salt) + if err != nil { + return nil, err + } + return o.decrypt(creds.Key, creds.IV, encryptedData) +} + +func (o OpenSSL) decrypt(key, iv, data []byte) ([]byte, error) { + if len(data) == 0 || len(data)%aes.BlockSize != 0 { + return nil, fmt.Errorf("bad blocksize(%v), aes.BlockSize = %v", len(data), aes.BlockSize) + } + c, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + cbc := cipher.NewCBCDecrypter(c, iv) + cbc.CryptBlocks(data[aes.BlockSize:], data[aes.BlockSize:]) + out, err := o.pkcs7Unpad(data[aes.BlockSize:], aes.BlockSize) + if out == nil { + return nil, err + } + return out, nil +} + +// EncryptBytes encrypts a slice of bytes that are base64 encoded in a manner compatible to OpenSSL encryption +// functions using AES-256-CBC as encryption algorithm. This function generates +// a random salt on every execution. +func (o OpenSSL) EncryptBytes(passphrase string, plainData []byte, cg CredsGenerator) ([]byte, error) { + salt, err := o.GenerateSalt() + if err != nil { + return nil, err + } + + return o.EncryptBytesWithSaltAndDigestFunc(passphrase, salt, plainData, cg) +} + +// EncryptBinaryBytes encrypts a slice of bytes in a manner compatible to OpenSSL encryption +// functions using AES-256-CBC as encryption algorithm. This function generates +// a random salt on every execution. +func (o OpenSSL) EncryptBinaryBytes(passphrase string, plainData []byte, cg CredsGenerator) ([]byte, error) { + salt, err := o.GenerateSalt() + if err != nil { + return nil, err + } + + return o.EncryptBinaryBytesWithSaltAndDigestFunc(passphrase, salt, plainData, cg) +} + +// EncryptBytesWithSaltAndDigestFunc encrypts a slice of bytes that are base64 encoded in a manner compatible to OpenSSL +// encryption functions using AES-256-CBC as encryption algorithm. The salt +// needs to be passed in here which ensures the same result on every execution +// on cost of a much weaker encryption as with EncryptString. +// +// The salt passed into this function needs to have exactly 8 byte. +// +// The hash function corresponds to the `-md` parameter of OpenSSL. For OpenSSL pre-1.1.0c +// DigestMD5Sum was the default, since then it is DigestSHA256Sum. +// +// If you don't have a good reason to use this, please don't! For more information +// see this: https://en.wikipedia.org/wiki/Salt_(cryptography)#Common_mistakes +func (o OpenSSL) EncryptBytesWithSaltAndDigestFunc(passphrase string, salt, plainData []byte, cg CredsGenerator) ([]byte, error) { + enc, err := o.EncryptBinaryBytesWithSaltAndDigestFunc(passphrase, salt, plainData, cg) + if err != nil { + return nil, err + } + + return []byte(base64.StdEncoding.EncodeToString(enc)), nil +} + +func (o OpenSSL) encrypt(key, iv, data []byte) ([]byte, error) { + padded, err := o.pkcs7Pad(data, aes.BlockSize) + if err != nil { + return nil, err + } + + c, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + cbc := cipher.NewCBCEncrypter(c, iv) + cbc.CryptBlocks(padded[aes.BlockSize:], padded[aes.BlockSize:]) + + return padded, nil +} + +// EncryptBinaryBytesWithSaltAndDigestFunc encrypts a slice of bytes in a manner compatible to OpenSSL +// encryption functions using AES-256-CBC as encryption algorithm. The salt +// needs to be passed in here which ensures the same result on every execution +// on cost of a much weaker encryption as with EncryptString. +// +// The salt passed into this function needs to have exactly 8 byte. +// +// The hash function corresponds to the `-md` parameter of OpenSSL. For OpenSSL pre-1.1.0c +// DigestMD5Sum was the default, since then it is DigestSHA256Sum. +// +// If you don't have a good reason to use this, please don't! For more information +// see this: https://en.wikipedia.org/wiki/Salt_(cryptography)#Common_mistakes +func (o OpenSSL) EncryptBinaryBytesWithSaltAndDigestFunc(passphrase string, salt, plainData []byte, cg CredsGenerator) ([]byte, error) { + if len(salt) != 8 { + return nil, ErrInvalidSalt + } + + data := make([]byte, len(plainData)+aes.BlockSize) + copy(data[0:], o.openSSLSaltHeader) + copy(data[8:], salt) + copy(data[aes.BlockSize:], plainData) + + creds, err := cg([]byte(passphrase), salt) + if err != nil { + return nil, err + } + + enc, err := o.encrypt(creds.Key, creds.IV, data) + if err != nil { + return nil, err + } + + return enc, nil +} + +// GenerateSalt generates a random 8 byte salt +func (o OpenSSL) GenerateSalt() ([]byte, error) { + salt := make([]byte, 8) // Generate an 8 byte salt + _, err := io.ReadFull(rand.Reader, salt) + if err != nil { + return nil, err + } + + return salt, nil +} + +// MustGenerateSalt is a wrapper around GenerateSalt which will panic on an error. +// This allows you to use this function as a parameter to EncryptBytesWithSaltAndDigestFunc +func (o OpenSSL) MustGenerateSalt() []byte { + s, err := o.GenerateSalt() + if err != nil { + panic(err) + } + return s +} + +// pkcs7Pad appends padding. +func (o OpenSSL) pkcs7Pad(data []byte, blocklen int) ([]byte, error) { + if blocklen <= 0 { + return nil, fmt.Errorf("invalid blocklen %d", blocklen) + } + padlen := 1 + for ((len(data) + padlen) % blocklen) != 0 { + padlen++ + } + + pad := bytes.Repeat([]byte{byte(padlen)}, padlen) + return append(data, pad...), nil +} + +// pkcs7Unpad returns slice of the original data without padding. +func (o OpenSSL) pkcs7Unpad(data []byte, blocklen int) ([]byte, error) { + if blocklen <= 0 { + return nil, fmt.Errorf("invalid blocklen %d", blocklen) + } + if len(data)%blocklen != 0 || len(data) == 0 { + return nil, fmt.Errorf("invalid data len %d", len(data)) + } + padlen := int(data[len(data)-1]) + if padlen > blocklen || padlen == 0 { + return nil, fmt.Errorf("invalid padding") + } + pad := data[len(data)-padlen:] + for i := 0; i < padlen; i++ { + if pad[i] != byte(padlen) { + return nil, fmt.Errorf("invalid padding") + } + } + return data[:len(data)-padlen], nil +} diff --git a/pkg/pillar/vendor/golang.org/x/crypto/pbkdf2/pbkdf2.go b/pkg/pillar/vendor/golang.org/x/crypto/pbkdf2/pbkdf2.go new file mode 100644 index 00000000000..904b57e01d7 --- /dev/null +++ b/pkg/pillar/vendor/golang.org/x/crypto/pbkdf2/pbkdf2.go @@ -0,0 +1,77 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/* +Package pbkdf2 implements the key derivation function PBKDF2 as defined in RFC +2898 / PKCS #5 v2.0. + +A key derivation function is useful when encrypting data based on a password +or any other not-fully-random data. It uses a pseudorandom function to derive +a secure encryption key based on the password. + +While v2.0 of the standard defines only one pseudorandom function to use, +HMAC-SHA1, the drafted v2.1 specification allows use of all five FIPS Approved +Hash Functions SHA-1, SHA-224, SHA-256, SHA-384 and SHA-512 for HMAC. To +choose, you can pass the `New` functions from the different SHA packages to +pbkdf2.Key. +*/ +package pbkdf2 // import "golang.org/x/crypto/pbkdf2" + +import ( + "crypto/hmac" + "hash" +) + +// Key derives a key from the password, salt and iteration count, returning a +// []byte of length keylen that can be used as cryptographic key. The key is +// derived based on the method described as PBKDF2 with the HMAC variant using +// the supplied hash function. +// +// For example, to use a HMAC-SHA-1 based PBKDF2 key derivation function, you +// can get a derived key for e.g. AES-256 (which needs a 32-byte key) by +// doing: +// +// dk := pbkdf2.Key([]byte("some password"), salt, 4096, 32, sha1.New) +// +// Remember to get a good random salt. At least 8 bytes is recommended by the +// RFC. +// +// Using a higher iteration count will increase the cost of an exhaustive +// search but will also make derivation proportionally slower. +func Key(password, salt []byte, iter, keyLen int, h func() hash.Hash) []byte { + prf := hmac.New(h, password) + hashLen := prf.Size() + numBlocks := (keyLen + hashLen - 1) / hashLen + + var buf [4]byte + dk := make([]byte, 0, numBlocks*hashLen) + U := make([]byte, hashLen) + for block := 1; block <= numBlocks; block++ { + // N.B.: || means concatenation, ^ means XOR + // for each block T_i = U_1 ^ U_2 ^ ... ^ U_iter + // U_1 = PRF(password, salt || uint(i)) + prf.Reset() + prf.Write(salt) + buf[0] = byte(block >> 24) + buf[1] = byte(block >> 16) + buf[2] = byte(block >> 8) + buf[3] = byte(block) + prf.Write(buf[:4]) + dk = prf.Sum(dk) + T := dk[len(dk)-hashLen:] + copy(U, T) + + // U_n = PRF(password, U_(n-1)) + for n := 2; n <= iter; n++ { + prf.Reset() + prf.Write(U) + U = U[:0] + U = prf.Sum(U) + for x := range U { + T[x] ^= U[x] + } + } + } + return dk[:keyLen] +} diff --git a/pkg/pillar/vendor/modules.txt b/pkg/pillar/vendor/modules.txt index bab0abe5711..65a4ad6a19f 100644 --- a/pkg/pillar/vendor/modules.txt +++ b/pkg/pillar/vendor/modules.txt @@ -30,6 +30,9 @@ github.com/Azure/azure-storage-blob-go/azblob ## explicit; go 1.16 github.com/Azure/go-ansiterm github.com/Azure/go-ansiterm/winterm +# github.com/Luzifer/go-openssl/v4 v4.1.0 +## explicit; go 1.14 +github.com/Luzifer/go-openssl/v4 # github.com/Microsoft/go-winio v0.5.2 ## explicit; go 1.13 github.com/Microsoft/go-winio @@ -705,6 +708,7 @@ golang.org/x/crypto/ed25519 golang.org/x/crypto/internal/alias golang.org/x/crypto/internal/poly1305 golang.org/x/crypto/ocsp +golang.org/x/crypto/pbkdf2 golang.org/x/crypto/ssh golang.org/x/crypto/ssh/internal/bcrypt_pbkdf # golang.org/x/net v0.7.0 diff --git a/pkg/wwan/Dockerfile b/pkg/wwan/Dockerfile index 13f8530175e..2f8b4e11933 100644 --- a/pkg/wwan/Dockerfile +++ b/pkg/wwan/Dockerfile @@ -3,15 +3,15 @@ FROM lfedge/eve-alpine:9fb9b9cbf7d90066a70e4704d04a6fe248ff52bb as build ENV BUILD_PKGS automake autoconf gettext gettext-dev git pkgconfig \ libtool libc-dev linux-headers gcc make glib-dev \ autoconf-archive patch cmake gtk-doc -ENV PKGS alpine-baselayout musl-utils ppp jq glib +ENV PKGS alpine-baselayout musl-utils ppp jq glib openssl RUN eve-alpine-deploy.sh ENV LIBUBOX_COMMIT=7da66430de3fc235bfc6ebb0b85fb90ea246138d ENV JSONC_COMMIT=ed54353d8478ccdb8296c33c675662d16d68b40d ENV INOTIFY_TOOLS_COMMIT=3.20.11.0 ENV PICOCOM_COMMIT=1acf1ddabaf3576b4023c4f6f09c5a3e4b086fb8 -ENV LIBQMI_COMMIT=1.26.2 -ENV LIBMBIM_COMMIT=1.24.2 +ENV LIBQMI_COMMIT=1.30.8 +ENV LIBMBIM_COMMIT=1.26.4 ADD --keep-git-dir=true https://github.com/json-c/json-c.git#${JSONC_COMMIT} /json-c WORKDIR /json-c @@ -29,8 +29,6 @@ RUN ./autogen.sh && ./configure --prefix=/usr && make -j "$(getconf _NPROCESSORS ADD --keep-git-dir=true https://gitlab.freedesktop.org/mobile-broadband/libqmi.git#${LIBQMI_COMMIT} /libqmi WORKDIR /libqmi -COPY patches/libqmi/*.patch /tmp/patches/libqmi/ -RUN for patch in /tmp/patches/libqmi/*.patch ; do patch -p1 < "$patch" ; done RUN ./autogen.sh --without-udev && ./configure --prefix=/usr --without-udev --enable-mbim-qmux && make -j "$(getconf _NPROCESSORS_ONLN)" && make install ADD --keep-git-dir=true https://github.com/inotify-tools/inotify-tools.git#${INOTIFY_TOOLS_COMMIT} /inotify-tools diff --git a/pkg/wwan/patches/libmbim/0002-core-drop-volatile-for-g_once_init_enter-locations.patch b/pkg/wwan/patches/libmbim/0002-core-drop-volatile-for-g_once_init_enter-locations.patch deleted file mode 100644 index 3659b6847c8..00000000000 --- a/pkg/wwan/patches/libmbim/0002-core-drop-volatile-for-g_once_init_enter-locations.patch +++ /dev/null @@ -1,96 +0,0 @@ -From e772a8103acd3ead338903307a036b63f2f63a51 Mon Sep 17 00:00:00 2001 -From: Aleksander Morgado -Date: Tue, 18 May 2021 11:09:54 +0200 -Subject: [PATCH] core: drop "volatile" for g_once_init_enter locations - -This fixes a few (fatal in gcc 11) warnings. - -See https://gitlab.gnome.org/GNOME/glib/-/issues/600 - -(cherry picked from commit 764d91155570d653e178ae8f12ff4de00d71e06c) ---- - build-aux/templates/mbim-enum-types-template.c | 8 ++++---- - build-aux/templates/mbim-error-types-template.c | 8 ++++---- - src/libmbim-glib/mbim-message.c | 8 ++++---- - 3 files changed, 12 insertions(+), 12 deletions(-) - -diff --git a/build-aux/templates/mbim-enum-types-template.c b/build-aux/templates/mbim-enum-types-template.c -index c59b798..fd3d47e 100644 ---- a/build-aux/templates/mbim-enum-types-template.c -+++ b/build-aux/templates/mbim-enum-types-template.c -@@ -21,16 +21,16 @@ static const G@Type@Value @enum_name@_values[] = { - GType - @enum_name@_get_type (void) - { -- static volatile gsize g_define_type_id__volatile = 0; -+ static gsize g_define_type_id_initialized = 0; - -- if (g_once_init_enter (&g_define_type_id__volatile)) { -+ if (g_once_init_enter (&g_define_type_id_initialized)) { - GType g_define_type_id = - g_@type@_register_static (g_intern_static_string ("@EnumName@"), - @enum_name@_values); -- g_once_init_leave (&g_define_type_id__volatile, g_define_type_id); -+ g_once_init_leave (&g_define_type_id_initialized, g_define_type_id); - } - -- return g_define_type_id__volatile; -+ return g_define_type_id_initialized; - } - - /* Enum-specific method to get the value as a string. -diff --git a/build-aux/templates/mbim-error-types-template.c b/build-aux/templates/mbim-error-types-template.c -index 7ed24b9..9f9589d 100644 ---- a/build-aux/templates/mbim-error-types-template.c -+++ b/build-aux/templates/mbim-error-types-template.c -@@ -20,16 +20,16 @@ static const G@Type@Value @enum_name@_values[] = { - GType - @enum_name@_get_type (void) - { -- static volatile gsize g_define_type_id__volatile = 0; -+ static gsize g_define_type_id_initialized = 0; - -- if (g_once_init_enter (&g_define_type_id__volatile)) { -+ if (g_once_init_enter (&g_define_type_id_initialized)) { - GType g_define_type_id = - g_@type@_register_static (g_intern_static_string ("@EnumName@"), - @enum_name@_values); -- g_once_init_leave (&g_define_type_id__volatile, g_define_type_id); -+ g_once_init_leave (&g_define_type_id_initialized, g_define_type_id); - } - -- return g_define_type_id__volatile; -+ return g_define_type_id_initialized; - } - - /* Enum-specific method to get the value as a string. -diff --git a/src/libmbim-glib/mbim-message.c b/src/libmbim-glib/mbim-message.c -index 7c30dd2..0d35647 100644 ---- a/src/libmbim-glib/mbim-message.c -+++ b/src/libmbim-glib/mbim-message.c -@@ -90,18 +90,18 @@ set_error_from_status (GError **error, - GType - mbim_message_get_type (void) - { -- static volatile gsize g_define_type_id__volatile = 0; -+ static gsize g_define_type_id_initialized = 0; - -- if (g_once_init_enter (&g_define_type_id__volatile)) { -+ if (g_once_init_enter (&g_define_type_id_initialized)) { - GType g_define_type_id = - g_boxed_type_register_static (g_intern_static_string ("MbimMessage"), - (GBoxedCopyFunc) mbim_message_ref, - (GBoxedFreeFunc) mbim_message_unref); - -- g_once_init_leave (&g_define_type_id__volatile, g_define_type_id); -+ g_once_init_leave (&g_define_type_id_initialized, g_define_type_id); - } - -- return g_define_type_id__volatile; -+ return g_define_type_id_initialized; - } - - /*****************************************************************************/ --- -2.34.1 - diff --git a/pkg/wwan/patches/libqmi/0001-core-drop-volatile-for-g_once_init_enter-locations.patch b/pkg/wwan/patches/libqmi/0001-core-drop-volatile-for-g_once_init_enter-locations.patch deleted file mode 100644 index 30495ddc8a5..00000000000 --- a/pkg/wwan/patches/libqmi/0001-core-drop-volatile-for-g_once_init_enter-locations.patch +++ /dev/null @@ -1,124 +0,0 @@ -From b24f8af1e8df9a81ea13dc403b8e5e58a14b5ea0 Mon Sep 17 00:00:00 2001 -From: Aleksander Morgado -Date: Tue, 18 May 2021 11:14:31 +0200 -Subject: [PATCH] core: drop "volatile" for g_once_init_enter locations - -This fixes a few (fatal in gcc 11) warnings. - -See https://gitlab.gnome.org/GNOME/glib/-/issues/600 - -(cherry picked from commit a80b1f1f25db0b81b25c45b3929975b68ac44ecb) ---- - build-aux/qmi-codegen/Container.py | 8 ++++---- - build-aux/templates/qmi-enum-types-template.c | 8 ++++---- - build-aux/templates/qmi-error-types-template.c | 8 ++++---- - src/libqmi-glib/qmi-message-context.c | 8 ++++---- - 4 files changed, 16 insertions(+), 16 deletions(-) - -diff --git a/build-aux/qmi-codegen/Container.py b/build-aux/qmi-codegen/Container.py -index 08daa33f..690d4fc5 100644 ---- a/build-aux/qmi-codegen/Container.py -+++ b/build-aux/qmi-codegen/Container.py -@@ -229,18 +229,18 @@ class Container: - '${static}GType\n' - '${underscore}_get_type (void)\n' - '{\n' -- ' static volatile gsize g_define_type_id__volatile = 0;\n' -+ ' static gsize g_define_type_id_initialized = 0;\n' - '\n' -- ' if (g_once_init_enter (&g_define_type_id__volatile)) {\n' -+ ' if (g_once_init_enter (&g_define_type_id_initialized)) {\n' - ' GType g_define_type_id =\n' - ' g_boxed_type_register_static (g_intern_static_string ("${camelcase}"),\n' - ' (GBoxedCopyFunc) ${underscore}_ref,\n' - ' (GBoxedFreeFunc) ${underscore}_unref);\n' - '\n' -- ' g_once_init_leave (&g_define_type_id__volatile, g_define_type_id);\n' -+ ' g_once_init_leave (&g_define_type_id_initialized, g_define_type_id);\n' - ' }\n' - '\n' -- ' return g_define_type_id__volatile;\n' -+ ' return g_define_type_id_initialized;\n' - '}\n' - '\n' - '${static}${camelcase} *\n' -diff --git a/build-aux/templates/qmi-enum-types-template.c b/build-aux/templates/qmi-enum-types-template.c -index 0e556896..d2670110 100644 ---- a/build-aux/templates/qmi-enum-types-template.c -+++ b/build-aux/templates/qmi-enum-types-template.c -@@ -21,16 +21,16 @@ static const G@Type@Value @enum_name@_values[] = { - GType - @enum_name@_get_type (void) - { -- static volatile gsize g_define_type_id__volatile = 0; -+ static gsize g_define_type_id_initialized = 0; - -- if (g_once_init_enter (&g_define_type_id__volatile)) { -+ if (g_once_init_enter (&g_define_type_id_initialized)) { - GType g_define_type_id = - g_@type@_register_static (g_intern_static_string ("@EnumName@"), - @enum_name@_values); -- g_once_init_leave (&g_define_type_id__volatile, g_define_type_id); -+ g_once_init_leave (&g_define_type_id_initialized, g_define_type_id); - } - -- return g_define_type_id__volatile; -+ return g_define_type_id_initialized; - } - - /* Enum-specific method to get the value as a string. -diff --git a/build-aux/templates/qmi-error-types-template.c b/build-aux/templates/qmi-error-types-template.c -index 2e138037..54e649de 100644 ---- a/build-aux/templates/qmi-error-types-template.c -+++ b/build-aux/templates/qmi-error-types-template.c -@@ -20,16 +20,16 @@ static const G@Type@Value @enum_name@_values[] = { - GType - @enum_name@_get_type (void) - { -- static volatile gsize g_define_type_id__volatile = 0; -+ static gsize g_define_type_id_initialized = 0; - -- if (g_once_init_enter (&g_define_type_id__volatile)) { -+ if (g_once_init_enter (&g_define_type_id_initialized)) { - GType g_define_type_id = - g_@type@_register_static (g_intern_static_string ("@EnumName@"), - @enum_name@_values); -- g_once_init_leave (&g_define_type_id__volatile, g_define_type_id); -+ g_once_init_leave (&g_define_type_id_initialized, g_define_type_id); - } - -- return g_define_type_id__volatile; -+ return g_define_type_id_initialized; - } - - /* Enum-specific method to get the value as a string. -diff --git a/src/libqmi-glib/qmi-message-context.c b/src/libqmi-glib/qmi-message-context.c -index 7461969c..f54e7d5c 100644 ---- a/src/libqmi-glib/qmi-message-context.c -+++ b/src/libqmi-glib/qmi-message-context.c -@@ -48,18 +48,18 @@ qmi_message_context_new (void) - GType - qmi_message_context_get_type (void) - { -- static volatile gsize g_define_type_id__volatile = 0; -+ static gsize g_define_type_id_initialized = 0; - -- if (g_once_init_enter (&g_define_type_id__volatile)) { -+ if (g_once_init_enter (&g_define_type_id_initialized)) { - GType g_define_type_id = - g_boxed_type_register_static (g_intern_static_string ("QmiMessageContext"), - (GBoxedCopyFunc) qmi_message_context_ref, - (GBoxedFreeFunc) qmi_message_context_unref); - -- g_once_init_leave (&g_define_type_id__volatile, g_define_type_id); -+ g_once_init_leave (&g_define_type_id_initialized, g_define_type_id); - } - -- return g_define_type_id__volatile; -+ return g_define_type_id_initialized; - } - - QmiMessageContext * --- -2.34.1 - diff --git a/pkg/wwan/usr/bin/wwan-init.sh b/pkg/wwan/usr/bin/wwan-init.sh index 47d062f966e..6536582973c 100755 --- a/pkg/wwan/usr/bin/wwan-init.sh +++ b/pkg/wwan/usr/bin/wwan-init.sh @@ -21,8 +21,8 @@ STATUS_PATH="${BBS}/status.json" METRICS_PATH="${BBS}/metrics.json" LOCINFO_PATH="${BBS}/location.json" -LTESTAT_TIMEOUT=120 -PROBE_INTERVAL=300 # how often to probe the connectivity status (in seconds) +CMD_TIMEOUT=5 # timeout applied for MBIM and QMI commands +PROBE_INTERVAL=20 # how often to probe the connectivity status (in seconds) METRICS_INTERVAL=60 # how often to obtain and publish metrics (in seconds) UNAVAIL_SIGNAL_METRIC=$(printf "%d" 0x7FFFFFFF) # max int32 @@ -31,6 +31,9 @@ DEFAULT_PROBE_ADDR="8.8.8.8" IPV4_REGEXP='^[0-9]\+\.[0-9]\+\.[0-9]\+\.[0-9]\+$' +# Used to encrypt user password between NIM and this microservice. +PASSPHRASE="$(cat /proc/sys/kernel/random/boot_id)" + SRC="$(cd "$(dirname "$0")" || exit 1; pwd)" # shellcheck source=./pkg/wwan/usr/bin/wwan-qmi.sh . "${SRC}/wwan-qmi.sh" @@ -63,6 +66,42 @@ parse_json_attr() { echo "$JSON" | jq -rc ".$JSON_PATH | select (.!=null)" } +join_lines_with_semicolon() { + cat - | sed ':a;N;$!ba;s/\n/; /g' +} + +escape_apostrophe() { + cat - | sed 's/\"/\\\"/g' +} + +join_lines_with_escaped_newline() { + cat - | sed ':a;N;$!ba;s/\n/\\n/g' +} + +bool_to_yesno() { + local INPUT + read -r INPUT + [ "$INPUT" = "true" ] && echo "yes" || echo "no" +} + +yesno_to_bool() { + local INPUT + read -r INPUT + [ "$INPUT" = "yes" ] && echo "true" || echo "false" +} + +bool_to_onoff() { + local INPUT + read -r INPUT + [ "$INPUT" = "true" ] && echo "on" || echo "off" +} + +onoff_to_bool() { + local INPUT + read -r INPUT + [ "$INPUT" = "on" ] && echo "true" || echo "false" +} + mod_reload() { local RLIST for mod in "$@" ; do @@ -76,12 +115,13 @@ mod_reload() { } wait_for() { - local EXPECT="$1" - shift - for i in $(seq 1 10); do + local ATTEMPTS="$1" + local EXPECT="$2" + shift 2 + for i in $(seq 1 "$ATTEMPTS"); do eval RES='"$('"$*"')"' [ "$RES" = "$EXPECT" ] && return 0 - sleep 6 + sleep 3 done return 1 } @@ -156,7 +196,18 @@ sys_get_modem_pciaddr() { # https://en.wikipedia.org/wiki/Hayes_command_set # Args: send_hayes_command() { - printf "%s\r\n" "$1" | picocom -qrx 2000 -b 9600 "$2" + if [ "$VERBOSE" = "true" ]; then + log_debug Running AT command: "$1" + fi + local OUTPUT + local RV + OUTPUT="$(printf "%s\r\n" "$1" | picocom -qrx 2000 -b 9600 "$2")" + RV=$? + if [ "$VERBOSE" = "true" ]; then + log_debug AT output: "$OUTPUT" + fi + echo "$OUTPUT" + return $RV } sys_get_modem_ttys() { @@ -178,29 +229,30 @@ sys_get_modem_atport() { return 1 } -# If successful, sets CDC_DEV, PROTOCOL, IFACE, USB_ADDR, PCI_ADDR and AT_PORT variables. +# If successful, sets CDC_DEV, PROTOCOL, IFACE, USB_ADDR and PCI_ADDR variables. lookup_modem() { local ARG_IF="$1" local ARG_USB="$2" local ARG_PCI="$3" for DEV in /sys/class/usbmisc/*; do - DEV_PROT=$(sys_get_modem_protocol "$DEV") || continue + if [ "$VERBOSE" = "true" ]; then + log_debug "lookup_modem: $DEV" + fi + local DEV_PROT=$(sys_get_modem_protocol "$DEV") || continue # check interface name - DEV_IF="$(sys_get_modem_interface "$DEV")" + local DEV_IF="$(sys_get_modem_interface "$DEV")" [ -n "$ARG_IF" ] && [ "$ARG_IF" != "$DEV_IF" ] && continue # check USB address - DEV_USB="$(sys_get_modem_usbaddr "$DEV")" + local DEV_USB="$(sys_get_modem_usbaddr "$DEV")" [ -n "$ARG_USB" ] && [ "$ARG_USB" != "$DEV_USB" ] && continue # check PCI address - DEV_PCI="$(sys_get_modem_pciaddr "$DEV")" + local DEV_PCI="$(sys_get_modem_pciaddr "$DEV")" [ -n "$ARG_PCI" ] && [ "$ARG_PCI" != "$DEV_PCI" ] && continue - AT_PORT="$(sys_get_modem_atport "$DEV_USB")" - PROTOCOL="$DEV_PROT" IFACE="$DEV_IF" USB_ADDR="$DEV_USB" @@ -209,23 +261,27 @@ lookup_modem() { return 0 done - echo "Failed to find modem for"\ - "interface=${ARG_IF:-}, USB=${ARG_USB:-}, PCI=${ARG_PCI:-}" >&2 + log_error "Failed to find modem for"\ + "interface=${ARG_IF:-}, USB=${ARG_USB:-}, PCI=${ARG_PCI:-}" return 1 } bringup_iface() { if ! "${PROTOCOL}_get_ip_settings"; then - echo "Failed to get IP config for interface $IFACE" + log_error "Failed to get IP config for interface $IFACE" return 1 fi ifconfig "$IFACE" "$IP" netmask "$SUBNET" pointopoint "$GW" if [ -n "$MTU" ]; then ip link set mtu "$MTU" dev "$IFACE" fi + local METRIC=65000 + # If interface name is something unexpected, metric will stay as 65000. + local IDX="${IFACE#"wwan"}" + METRIC="$((METRIC+IDX))" # NOTE we may want to disable /proc/sys/net/ipv4/conf/default/rp_filter instead # Verify it by cat /proc/net/netstat | awk '{print $80}' - ip route add default via "$GW" dev "$IFACE" metric 65000 + ip route add default via "$GW" dev "$IFACE" metric "${METRIC}" mkdir -p "$BBS/resolv.conf" local RESOLV_CONF="$BBS/resolv.conf/${IFACE}.dhcp" : > "$RESOLV_CONF" @@ -246,33 +302,54 @@ bringup_iface() { } bringdown_iface() { - # Truncate resolv.conf if it exists. + # Remove resolv.conf if it exists. local RESOLV_CONF="$BBS/resolv.conf/${IFACE}.dhcp" - if [ -f "$RESOLV_CONF" ]; then - : > "$RESOLV_CONF" - fi + rm -f "$RESOLV_CONF" # Remove IP address and routes from the interface. ip addr flush dev "$IFACE" } +# Sets global variable DP_STUCK to "y" if data-plane is found to be stuck. check_connectivity() { + local EVENT="$1" + unset PROBE_ERROR # First check the connectivity status as reported by the modem. - if [ "$("${PROTOCOL}_get_op_mode")" != "online-and-connected" ]; then + local OP_STATUS="$("${PROTOCOL}_get_op_mode")" + if [ "$OP_STATUS" != "online-and-connected" ]; then + add_probe_error "modem is not connected (operational mode: ${OP_STATUS})" return 1 fi + if "${PROTOCOL}_get_ip_address" | grep -vq "$IPV4_REGEXP"; then + add_probe_error "no IP address assigned" + return 1 + fi + if [ "$EVENT" = "QUICK-PROBE" ]; then + # Do not generate any traffic during a quick probe. + # Assume that the connectivity is OK if modem says so. + return 0 + fi # (optionally) Check connectivity by communicating with a remote endpoint. - probe_connectivity + PS_BEFORE="$("${PROTOCOL}_get_packet_stats")" + if ! probe_connectivity; then + PS_AFTER="$("${PROTOCOL}_get_packet_stats")" + # If packet counters as reported by the modem has not changed, + # then data-plane is very likely stuck. + if [ "$PS_BEFORE" != "$PS_AFTER" ]; then + DP_STUCK=y + add_probe_error "data-plane of the modem is stuck" + fi + return 1 + fi } probe_connectivity() { - unset PROBE_ERROR if [ "$PROBE_DISABLED" = "true" ]; then # probing disabled, skip it return 0 fi if [ -n "$PROBE_ADDR" ]; then # User-configured ICMP probe address. - add_probe_error "$(icmp_probe "$PROBE_ADDR")" + icmp_probe "$PROBE_ADDR" return fi # Default probing behaviour (not configured by user). @@ -309,63 +386,102 @@ __EOT__ # This is a last-resort probing option. # In a private LTE network ICMP requests headed towards public DNS servers # may be blocked by the firewall and thus produce probing false negatives. - add_probe_error "$(icmp_probe "$DEFAULT_PROBE_ADDR")" + icmp_probe "$DEFAULT_PROBE_ADDR" } add_probe_error() { if [ -z "$1" ]; then return fi + local ERR_MSG="$(echo "$1" | join_lines_with_semicolon | escape_apostrophe)" if [ -n "$PROBE_ERROR" ]; then - PROBE_ERROR="$PROBE_ERROR; $1" + PROBE_ERROR="$PROBE_ERROR; $ERR_MSG" else - PROBE_ERROR="$1" + PROBE_ERROR="$ERR_MSG" + fi + if [ "$VERBOSE" = "true" ]; then + log_debug "[$CDC_DEV] Check connectivity: $1" fi } icmp_probe() { - local PROBE_ADDR="$1" + local PING_ADDR="$1" # ping is supposed to return 0 even if just a single packet out of 3 gets through - local PROBE_OUTPUT - if PROBE_OUTPUT="$(ping -W 20 -w 20 -c 3 -I "$IFACE" "$PROBE_ADDR" 2>&1)"; then + local PING_OUTPUT + if PING_OUTPUT="$(ping -W 20 -w 20 -c 3 -I "$IFACE" "$PING_ADDR" 2>&1)"; then return 0 else - local PROBE_ERROR="$(printf "%s" "$PROBE_OUTPUT" | grep "packet loss")" - if [ -z "$PROBE_ERROR" ]; then - PROBE_ERROR="$PROBE_OUTPUT" + local PING_ERROR="$(printf "%s" "$PING_OUTPUT" | grep "packet loss")" + if [ -z "$PING_ERROR" ]; then + PING_ERROR="$PING_OUTPUT" fi - echo "Failed to ping $PROBE_ADDR via $IFACE: $PROBE_ERROR" + add_probe_error "Failed to ping $PING_ADDR via $IFACE: $PING_ERROR" return 1 fi } collect_network_status() { - local QUICK="$1" + local EVENT="$1" local PROVIDERS="[]" - if [ "$QUICK" != "y" ]; then + if [ "$EVENT" = "LONG-PROBE" ]; then # The process of scanning for available providers takes up to 1 minute. - # It is done only during PROBING events and skipped when config is changed + # It is done only during LONG-PROBE events and skipped when config is changed # (e.g. radio-silence mode is switched ON/OFF) so that the updated status is promptly # published for better user experience. PROVIDERS="$("${PROTOCOL}_get_providers")" + else + # Just preserve the list of providers previously obtained for this modem. + PROVIDERS="$(jq -rc --arg CDC_DEV "$CDC_DEV" \ + '.networks[] | select(."physical-addrs".dev==$CDC_DEV) | ."visible-providers"' \ + "${STATUS_PATH}" 2>/dev/null)" + PROVIDERS="${PROVIDERS:-"[]"}" fi + local OP_MODE="$("${PROTOCOL}_get_op_mode")" local MODULE="$(json_struct \ "$(json_str_attr imei "$("${PROTOCOL}_get_imei")")" \ "$(json_str_attr model "$("${PROTOCOL}_get_modem_model")")" \ "$(json_str_attr revision "$("${PROTOCOL}_get_modem_revision")")" \ + "$(json_str_attr manufacturer "$("${PROTOCOL}_get_modem_manufacturer")")" \ "$(json_str_attr control-protocol "$PROTOCOL")" \ - "$(json_str_attr operating-mode "$("${PROTOCOL}_get_op_mode")")")" + "$(json_str_attr operating-mode "$OP_MODE")")" local NETWORK_STATUS="$(json_struct \ - "$(json_str_attr logical-label "$LOGICAL_LABEL")" \ - "$(json_attr physical-addrs "$ADDRS")" \ - "$(json_attr cellular-module "$MODULE")" \ - "$(json_attr sim-cards "$("${PROTOCOL}_get_sim_cards")")" \ - "$(json_str_attr config-error "$CONFIG_ERROR")" \ - "$(json_str_attr probe-error "$PROBE_ERROR")" \ - "$(json_attr visible-providers "$PROVIDERS")")" + "$(json_str_attr logical-label "$LOGICAL_LABEL")" \ + "$(json_attr physical-addrs "$ADDRS")" \ + "$(json_attr cellular-module "$MODULE")" \ + "$(json_attr sim-cards "$("${PROTOCOL}_get_sim_cards")")" \ + "$(json_str_attr config-error "$CONFIG_ERROR")" \ + "$(json_str_attr probe-error "$PROBE_ERROR")" \ + "$(json_attr visible-providers "$PROVIDERS")" \ + "$(json_attr current-provider "$("${PROTOCOL}_get_current_provider")")" \ + "$(json_attr current-rats "$("${PROTOCOL}_get_currently_used_rats")")" \ + "$(json_attr connected-at "$(get_connection_time)")" \ + "$(json_attr suspended-quickprobe "${SUSPEND_QUICKPROBE:-false}")")" STATUS="${STATUS}${NETWORK_STATUS}\n" } +# When modem is connected, the corresponding resolv.conf file is created and/or updated +# (at least emptied). +# Disconnected modem does not have resolv.conf. +get_connection_time() { + local TIMESTAMP="$(stat -c "%Y" "$BBS/resolv.conf/${IFACE}.dhcp" 2>/dev/null)" + TIMESTAMP="${TIMESTAMP:-0}" + echo "$TIMESTAMP" +} + +is_quickprobe_suspended() { + local SUSPENDED="$(jq -rc --arg CDC_DEV "$CDC_DEV" \ + '.networks[] | select(."physical-addrs".dev==$CDC_DEV) | ."suspended-quickprobe"' \ + "${STATUS_PATH}" 2>/dev/null)" + [ "$SUSPENDED" = "true" ] && return 0 || return 1 +} + +sim_card_status_changed() { + local PREV_STATUS="$(jq -rc --arg CDC_DEV "$CDC_DEV" \ + '.networks[] | select(."physical-addrs".dev==$CDC_DEV) | ."sim-cards"' \ + "${STATUS_PATH}" 2>/dev/null)" + [ "$PREV_STATUS" != "$("${PROTOCOL}_get_sim_cards")" ] && return 0 || return 1 +} + collect_network_metrics() { local NETWORK_METRICS="$(json_struct \ "$(json_str_attr logical-label "$LOGICAL_LABEL")" \ @@ -391,15 +507,16 @@ switch_to_preferred_proto() { # attempts. Therefore we prefer to use MBIM with this modem until we find # a better solution. if [ "$PROTOCOL" != "mbim" ]; then + local AT_PORT="$(sys_get_modem_atport "$USB_ADDR")" if [ -z "$AT_PORT" ]; then - echo "Cannot switch modem $LOGICAL_LABEL to MBIM: AT port is not available" + log_error "Cannot switch modem $LOGICAL_LABEL to MBIM: AT port is not available" return 1 fi - echo "Switching modem $LOGICAL_LABEL to MBIM (using AT port: ${AT_PORT})..." + log_debug "Switching modem $LOGICAL_LABEL to MBIM (using AT port: ${AT_PORT})..." for CMD in '+++' 'AT!ENTERCND="A710"' 'AT!USBCOMP=1,1,100D' 'AT!RESET'; do local OUT="$(send_hayes_command "$CMD" "${AT_PORT}")" if echo "$OUT" | grep -q "ERROR"; then - echo "Failed to switch modem $LOGICAL_LABEL to MBIM: $OUT" + log_error "Failed to switch modem $LOGICAL_LABEL to MBIM: $OUT" return 1 fi done @@ -410,19 +527,110 @@ switch_to_preferred_proto() { return 1 # No switch executed. } +# According to ETSI TS 100 916 V7.4.0 (1999-11), Section 8.2, +# "AT+CFUN=1,1" is one of the commands a modem should implement. +reset_modem_method1() { + local AT_PORT="$1" + for CMD in '+++' 'AT+CFUN=1,1'; do + local OUT="$(send_hayes_command "$CMD" "${AT_PORT}")" + if echo "$OUT" | grep -q "ERROR"; then + log_error "Failed to reset modem $LOGICAL_LABEL using AT+CFUN=1,1: $OUT" + return 1 + fi + done +} + +# Just for a rare case that "AT+CFUN=1,1" is not available, we try also AT!RESET, +# which, however, is Sierra Wireless specific. +reset_modem_method2() { + local AT_PORT="$1" + for CMD in '+++' 'AT!RESET'; do + local OUT="$(send_hayes_command "$CMD" "${AT_PORT}")" + if echo "$OUT" | grep -q "ERROR"; then + log_error "Failed to reset modem $LOGICAL_LABEL using" 'AT!RESET' ": $OUT" + return 1 + fi + done +} + +reset_modem() { + local AT_PORT="$(sys_get_modem_atport "$USB_ADDR")" + if [ -z "$AT_PORT" ]; then + log_error "Cannot reset modem $LOGICAL_LABEL: AT port is not available" + return 1 + fi + log_debug "Resetting modem $LOGICAL_LABEL (using AT port: ${AT_PORT})..." + reset_modem_method1 "$AT_PORT" || reset_modem_method2 "$AT_PORT" || return 1 +} + +LOG_PIPE="/tmp/wwan-log.pipe" +if [ ! -p "$LOG_PIPE" ]; then + rm -f "$LOG_PIPE" + mkfifo "$LOG_PIPE" +fi + +log_debug() { + local MSG="${@}" + echo "$MSG" | join_lines_with_escaped_newline > "$LOG_PIPE" + echo "$(date "+%T") $MSG" | join_lines_with_escaped_newline >> "/tmp/wwan.log" # TODO: remove +} + +log_error() { + local MSG="${@}" + echo "Error: $MSG" | join_lines_with_escaped_newline > "$LOG_PIPE" + echo "$(date "+%T") Error: $MSG" | join_lines_with_escaped_newline >> "/tmp/wwan.log" # TODO: remove + # Additionally to logging, print the error message to stderr. + # This is then typically redirected to /tmp/wwan.stderr and loaded into CONFIG_ERROR + # variable. + echo "$MSG" >&2 +} + +logger() { + while [ -p "$LOG_PIPE" ]; do cat "$LOG_PIPE"; done +} + +logger & + +# Suspend periodic events, i.e. probes and metrics (not config change notifications). +# This is used to avoid long backlog of pending periodic events. +suspend_periodic_events() { + touch "${BBS}/${1}.suspend" +} + +release_periodic_events() { + rm -f "${BBS}/${1}.suspend" +} + +wait_if_suspended() { + local WAS_SUSPENDED + while [ -f "${BBS}/${1}.suspend" ]; do + WAS_SUSPENDED="y" + sleep 1 + done + if [ "$WAS_SUSPENDED" = "y" ]; then + # Processing of an event just completed - do not immediately trigger another one. + sleep 5 + fi +} + event_stream() { - inotifywait -qm "${BBS}" --include config.json -e create -e modify -e delete -e moved_to & + inotifywait -qm "${BBS}" --exclude location -e create -e modify -e delete -e moved_to & while true; do + wait_if_suspended "probe" echo "PROBE" sleep "$PROBE_INTERVAL" done & + # Do not ask for metrics immediately after boot (may delay applying initial config), + # instead wait 2 minutes. + sleep 120 while true; do + wait_if_suspended "metrics" echo "METRICS" sleep "$METRICS_INTERVAL" done } -echo "Starting wwan manager" +log_debug "Starting wwan manager" mkdir -p "${BBS}" modprobe -a qcserial usb_wwan qmi_wwan cdc_wdm cdc_mbim cdc_acm @@ -435,18 +643,44 @@ modprobe -a qcserial usb_wwan qmi_wwan cdc_wdm cdc_mbim cdc_acm rfkill unblock wwan # Main event loop +PROBE_ITER=0 +ENFORCE_LONG_PROBE=n +STATUS_OUTDATED=n event_stream | while read -r EVENT; do if ! echo "$EVENT" | grep -q "PROBE\|METRICS\|config.json"; then continue fi - CONFIG_CHANGE=n if [ "$EVENT" != "PROBE" ] && [ "$EVENT" != "METRICS" ]; then - CONFIG_CHANGE=y + EVENT="CONFIG-CHANGE" + # Next probe will update the set of visible/used providers. + ENFORCE_LONG_PROBE="y" + fi + + if [ "$EVENT" = "PROBE" ]; then + PROBE_ITER="$((PROBE_ITER+1))" + # Every 30 seconds check the modem connectivity status. + # Quick probe only checks the status as reported by the modem, + # without generating any traffic. + EVENT="QUICK-PROBE" + if [ "$((PROBE_ITER % 15))" = "0" ] || [ "$STATUS_OUTDATED" = "y" ]; then + # Every 5 minutes update status.json. + # Also when QUICK-PROBE changes modem status (e.g. reconnects), next PROBE + # will be elevated to at least STANDARD-PROBE level. + # First update is not done immediately but after 5 minutes (PROBE_ITER starts with 1). + EVENT="STANDARD-PROBE" + fi + if [ "$((PROBE_ITER % 180))" = "31" ] || [ "$ENFORCE_LONG_PROBE" = "y" ]; then + # Every 1 hour additionally query the set of visible providers. + # Also after processing config change, next PROBE will be elevated to LONG-PROBE level. + # First LONG-PROBE is done after 10 minutes (modulo equals 31; PROBE_ITER starts with 1). + EVENT="LONG-PROBE" + fi + ENFORCE_LONG_PROBE=n fi CONFIG="$(cat "${CONFIG_PATH}" 2>/dev/null)" - if [ "$CONFIG_CHANGE" = "y" ]; then + if [ "$EVENT" = "CONFIG-CHANGE" ]; then if [ "$LAST_CONFIG" = "$CONFIG" ]; then # spurious notification, ignore continue @@ -463,12 +697,25 @@ event_stream | while read -r EVENT; do unset LOC_TRACKING_PROTO unset LOC_TRACKING_LL RADIO_SILENCE="$(parse_json_attr "$CONFIG" "\"radio-silence\"")" + VERBOSE="$(parse_json_attr "$CONFIG" "\"verbose\"")" + + if [ "$VERBOSE" = "true" ]; then + # TODO: remove this extra lines + log_debug + log_debug + log_debug Event: "$EVENT" + fi + + # Avoid periodic events getting backlogged while this one completes. + suspend_periodic_events "probe" + suspend_periodic_events "metrics" # iterate over each configured cellular network while read -r NETWORK; do [ -z "$NETWORK" ] && continue unset CONFIG_ERROR unset PROBE_ERROR + unset SUSPEND_QUICKPROBE # parse network configuration LOGICAL_LABEL="$(parse_json_attr "$NETWORK" "\"logical-label\"")" @@ -480,23 +727,57 @@ event_stream | while read -r EVENT; do PROBE_DISABLED="$(parse_json_attr "$PROBE" "disable")" PROBE_ADDR="$(parse_json_attr "$PROBE" "address")" PROXIES="$(parse_json_attr "$NETWORK" "proxies")" + SIM_SLOT="$(parse_json_attr "$NETWORK" "\"sim-slot\"")" APN="$(parse_json_attr "$NETWORK" "apn")" APN="${APN:-$DEFAULT_APN}" + AUTH_PROTO="$(parse_json_attr "$NETWORK" "\"auth-protocol\"")" + USERNAME="$(parse_json_attr "$NETWORK" "username")" + ENC_PASSWORD="$(parse_json_attr "$NETWORK" "\"encrypted-password\"")" + PREFERRED_PLMNS="$(parse_json_attr "$NETWORK" "\"preferred-plmns\"")" + PREFERRED_RATS="$(parse_json_attr "$NETWORK" "\"preferred-rats\"")" + FORBID_ROAMING="$(parse_json_attr "$NETWORK" "\"forbid-roaming\"")" LOC_TRACKING="$(parse_json_attr "$NETWORK" "\"location-tracking\"")" if ! lookup_modem "${IFACE}" "${USB_ADDR}" "${PCI_ADDR}" 2>/tmp/wwan.stderr; then - CONFIG_ERROR="$(cat /tmp/wwan.stderr)" + CONFIG_ERROR="$(join_lines_with_semicolon /dev/null; then + CONFIG_ERROR="Modem $LOGICAL_LABEL is not responsive" + log_debug "$CONFIG_ERROR" + NETWORK_STATUS="$(json_struct \ + "$(json_str_attr logical-label "$LOGICAL_LABEL")" \ + "$(json_attr physical-addrs "$ADDRS")" \ + "$(json_str_attr config-error "$CONFIG_ERROR")")" + STATUS="${STATUS}${NETWORK_STATUS}\n" + if [ "$(get_connection_time)" != "0" ]; then + bringdown_iface + STATUS_OUTDATED=y + fi + if [ "$EVENT" != "QUICK-PROBE" ] && [ "$EVENT" != "METRICS" ]; then + reset_modem + # Do not wait for reset to complete, return back to this modem later + # (e.g. in the next probing event). + fi + continue + fi - if switch_to_preferred_proto; then + if switch_to_preferred_proto 2>/dev/null; then # Modem is being restarted, return back to it later. continue fi @@ -506,7 +787,8 @@ event_stream | while read -r EVENT; do ADDRS="$(json_struct \ "$(json_str_attr interface "$IFACE")" \ "$(json_str_attr usb "$USB_ADDR")" \ - "$(json_str_attr pci "$PCI_ADDR")")" + "$(json_str_attr pci "$PCI_ADDR")" \ + "$(json_str_attr dev "$CDC_DEV")")" if [ "$EVENT" = "METRICS" ]; then collect_network_metrics 2>/dev/null @@ -519,38 +801,97 @@ event_stream | while read -r EVENT; do LOC_TRACKING_LL="$LOGICAL_LABEL" fi + if [ "$EVENT" = "QUICK-PROBE" ] && is_quickprobe_suspended; then + if sim_card_status_changed; then + log_debug "[$CDC_DEV] Quick-probe is suspended but SIM card status changed" + else + [ "$VERBOSE" = "true" ] && log_debug "[$CDC_DEV] Quick-probe is suspended" + continue + fi + fi + # reflect updated config or just probe the current status if [ "$RADIO_SILENCE" != "true" ]; then - if [ "$CONFIG_CHANGE" = "y" ] || ! check_connectivity; then - echo "[$CDC_DEV] Restarting connection (APN=${APN}, interface=${IFACE})" + DP_STUCK="n" + if [ "$EVENT" = "CONFIG-CHANGE" ] || ! check_connectivity "$EVENT"; then + if [ "$(get_connection_time)" != "0" ]; then + bringdown_iface + # Connectivity just stopped working or config changed. + # Ensure that status.json is updated during this event or by the next probe. + STATUS_OUTDATED=y + fi + if [ "$DP_STUCK" = "y" ]; then + if [ "$EVENT" != "QUICK-PROBE" ]; then + # Reset modem to recover + bringdown_iface + reset_modem + fi + NETWORK_STATUS="$(json_struct \ + "$(json_str_attr logical-label "$LOGICAL_LABEL")" \ + "$(json_attr physical-addrs "$ADDRS")" \ + "$(json_str_attr probe-error "$PROBE_ERROR")")" + STATUS="${STATUS}${NETWORK_STATUS}\n" + continue + fi + if [ -n "$USERNAME" ]; then + log_debug "[$CDC_DEV] Restarting connection (NETWORK=${LOGICAL_LABEL}, APN=${APN}, " \ + "username=${USERNAME}, auth-proto=${AUTH_PROTO})" + # Try to decrypt user password. + PASSWORD="$(echo "$ENC_PASSWORD" |\ + openssl enc -base64 -d -aes-256-cbc -pass "pass:$PASSPHRASE" -pbkdf2)" + log_debug "[$CDC_DEV] User password is: ${PASSWORD}" # TODO: remove + RV=$? + if [ $RV -ne 0 ]; then + CONFIG_ERROR="failed to decrypt user password (rv=$RV)" + NETWORK_STATUS="$(json_struct \ + "$(json_str_attr logical-label "$LOGICAL_LABEL")" \ + "$(json_attr physical-addrs "$ADDRS")" \ + "$(json_str_attr config-error "$CONFIG_ERROR")")" + STATUS="${STATUS}${NETWORK_STATUS}\n" + continue + fi + else + log_debug "[$CDC_DEV] Restarting connection (NETWORK=${LOGICAL_LABEL}, APN=${APN})" + fi { - bringdown_iface &&\ - "${PROTOCOL}_stop_network" &&\ - "${PROTOCOL}_toggle_rf" on &&\ - "${PROTOCOL}_wait_for_sim" &&\ - "${PROTOCOL}_wait_for_register" &&\ - "${PROTOCOL}_start_network" &&\ - "${PROTOCOL}_wait_for_wds" &&\ - "${PROTOCOL}_wait_for_settings" &&\ - bringup_iface &&\ - echo "[$CDC_DEV] Connection successfully restarted" + bringdown_iface &&\ + "${PROTOCOL}_stop_network" &&\ + "${PROTOCOL}_toggle_rf" on &&\ + "${PROTOCOL}_wait_for_sim" &&\ + "${PROTOCOL}_wait_for_register" &&\ + "${PROTOCOL}_start_network" &&\ + "${PROTOCOL}_wait_for_wds" &&\ + "${PROTOCOL}_wait_for_ip_config" &&\ + bringup_iface &&\ + STATUS_OUTDATED=y &&\ + log_debug "[$CDC_DEV] Connection successfully restarted (NETWORK=${LOGICAL_LABEL})" } 2>/tmp/wwan.stderr RV=$? if [ $RV -ne 0 ]; then - CONFIG_ERROR="$(sort -u < /tmp/wwan.stderr)" + CONFIG_ERROR="$(sort -u < /tmp/wwan.stderr | join_lines_with_semicolon | escape_apostrophe)" CONFIG_ERROR="${CONFIG_ERROR:-(Re)Connection attempt failed with rv=$RV}" + # Avoid frequent reconnection attempts by disabling quick probe until Standard + # or Long probe fixes the current connection problem. + # Suspend is bypassed only if we detect change in the SIM card status + # (e.g. SIM card was inserted, replaced, etc.). + SUSPEND_QUICKPROBE="true" fi # retry probe to update PROBE_ERROR - sleep 3 - probe_connectivity + if [ "$EVENT" != "QUICK-PROBE" ]; then + sleep 3 + if ! check_connectivity "$EVENT"; then + SUSPEND_QUICKPROBE="true" + fi + fi fi else # Radio-silence is ON if [ "$("${PROTOCOL}_get_op_mode")" != "radio-off" ]; then - echo "[$CDC_DEV] Trying to disable radio (APN=${APN}, interface=${IFACE})" + log_debug "[$CDC_DEV] Trying to disable radio (network=${LOGICAL_LABEL})" + STATUS_OUTDATED=y if ! "${PROTOCOL}_toggle_rf" off 2>/tmp/wwan.stderr; then - CONFIG_ERROR="$(cat /tmp/wwan.stderr)" + CONFIG_ERROR="$(join_lines_with_semicolon /dev/null) __EOT__ # Start/stop location tracking. - if [ "$CONFIG_CHANGE" = "y" ]; then + if [ "$EVENT" = "CONFIG-CHANGE" ]; then if [ -n "$LOC_TRACKING_DEV" ]; then if [ -z "$LOC_TRACKER" ]; then location_tracking "${LOC_TRACKING_LL}" "${LOC_TRACKING_DEV}"\ @@ -575,7 +918,7 @@ __EOT__ else if [ -n "$LOC_TRACKER" ]; then kill_process_tree $LOC_TRACKER >/dev/null 2>&1 - echo "Location tracking was stopped (parent process: $LOC_TRACKER)" + log_debug "Location tracking was stopped (parent process: $LOC_TRACKER)" unset LOC_TRACKER fi fi @@ -586,6 +929,7 @@ __EOT__ unset CONFIG_ERROR unset PROBE_ERROR unset LOGICAL_LABEL # unmanaged modems do not have logical name + unset SUSPEND_QUICKPROBE PROTOCOL="$(sys_get_modem_protocol "$DEV")" || continue CDC_DEV="$(basename "${DEV}")" @@ -593,36 +937,54 @@ __EOT__ # this modem has configuration and was already processed continue fi - echo "Processing unmanaged modem (event: $EVENT): $CDC_DEV" + if [ "$VERBOSE" = "true" ]; then + log_debug "Processing unmanaged modem (event: $EVENT): $CDC_DEV" + fi IFACE=$(sys_get_modem_interface "$DEV") USB_ADDR=$(sys_get_modem_usbaddr "$DEV") PCI_ADDR=$(sys_get_modem_pciaddr "$DEV") ADDRS="$(json_struct \ "$(json_str_attr interface "$IFACE")" \ "$(json_str_attr usb "$USB_ADDR")" \ - "$(json_str_attr pci "$PCI_ADDR")")" + "$(json_str_attr pci "$PCI_ADDR")" \ + "$(json_str_attr dev "$CDC_DEV")")" if [ "$EVENT" = "METRICS" ]; then collect_network_metrics 2>/dev/null continue fi + if [ "$(get_connection_time)" != "0" ]; then + bringdown_iface + STATUS_OUTDATED=y + fi + if [ "$("${PROTOCOL}_get_op_mode")" != "radio-off" ]; then - echo "[$CDC_DEV] Trying to disable radio (interface=${IFACE})" + log_debug "[$CDC_DEV] Trying to disable radio" + STATUS_OUTDATED=y if ! "${PROTOCOL}_toggle_rf" off 2>/tmp/wwan.stderr; then - CONFIG_ERROR="$(cat /tmp/wwan.stderr)" + CONFIG_ERROR="$(join_lines_with_semicolon "${STATUS_PATH}.tmp" + | tee /tmp/wwan-status.json | jq > "${STATUS_PATH}.tmp" # update status atomically mv "${STATUS_PATH}.tmp" "${STATUS_PATH}" + STATUS_OUTDATED=n fi done diff --git a/pkg/wwan/usr/bin/wwan-loc.sh b/pkg/wwan/usr/bin/wwan-loc.sh index 67c2abde1c4..525f7d7f97f 100644 --- a/pkg/wwan/usr/bin/wwan-loc.sh +++ b/pkg/wwan/usr/bin/wwan-loc.sh @@ -87,7 +87,7 @@ publish_location() { # Update location atomically. mv "${OUTPUT}.tmp" "${OUTPUT}" done - echo "Location publisher stopped" + log_debug "Location publisher stopped" } # Function keeps publishing location updates to /run/wwan/location.json @@ -109,7 +109,7 @@ location_tracking() { while true; do if [ "$FIRST_ATTEMPT" = "n" ]; then sleep 1 # Maybe intentionally killed, wait before claiming that we will retry. - echo "Retrying location tracking after $RETRY_AFTER seconds..." + log_debug "Retrying location tracking after $RETRY_AFTER seconds..." sleep $RETRY_AFTER fi FIRST_ATTEMPT=n @@ -119,20 +119,20 @@ location_tracking() { if ! LOC_START="$(timeout -s KILL 60 qmicli -p "--device-open-$PROTOCOL" \ -d "/dev/$CDC_DEV" --loc-start \ --client-no-release-cid)"; then - echo "Failed to start location service" + log_error "Failed to start location service" continue fi CID=$(echo "$LOC_START" | sed -n "s/\s*CID: '\(.*\)'/\1/p") - echo "Location tracking CID is $CID" + log_debug "Location tracking CID is $CID" publish_location "$LOGICAL_LABEL" "$PIPE" "$OUTPUT_FILE" & PUBLISHER_PID=$! - echo "PID of the location publisher is $PUBLISHER_PID" + log_debug "PID of the location publisher is $PUBLISHER_PID" qmicli -p "--device-open-$PROTOCOL" -d "/dev/$CDC_DEV" \ --loc-follow-position-report "--client-cid=$CID" >"$PIPE" 2>"$STDERR" & TRACKER_PID=$! - echo "PID of the location tracker is $TRACKER_PID" + log_debug "PID of the location tracker is $TRACKER_PID" # Watchdog - we expect at least one location update every minute, # otherwise we consider the location tracking to be stuck. @@ -140,12 +140,12 @@ location_tracking() { while true; do sleep 60 if [ ! -f "$OUTPUT_FILE" ]; then - echo "Location info is not available, restarting tracker" + log_debug "Location info is not available, restarting tracker" break fi NEW_MODTIME="$(date "+%s" -r "$OUTPUT_FILE" 2>/dev/null)" if [ "$MODTIME" = "$NEW_MODTIME" ]; then - echo "Location info has not been updated in the last minute, restarting tracker" + log_debug "Location info has not been updated in the last minute, restarting tracker" break fi MODTIME="$NEW_MODTIME" @@ -154,7 +154,7 @@ location_tracking() { # Stop location tracking - it is likely stuck. kill_process_tree $PUBLISHER_PID >/dev/null 2>&1 kill_process_tree $TRACKER_PID >/dev/null 2>&1 - echo "Location tracking was killed" + log_debug "Location tracking was killed" cat "$STDERR" # Release client CID diff --git a/pkg/wwan/usr/bin/wwan-mbim.sh b/pkg/wwan/usr/bin/wwan-mbim.sh index c56751e5bd8..67afa331dc4 100644 --- a/pkg/wwan/usr/bin/wwan-mbim.sh +++ b/pkg/wwan/usr/bin/wwan-mbim.sh @@ -4,7 +4,25 @@ # shellcheck disable=SC2034 mbim() { - timeout -s INT -k 5 "$LTESTAT_TIMEOUT" mbimcli -p -d "/dev/$CDC_DEV" "$@" + if [ "$VERBOSE" = "true" ]; then + # Do not log user password. + local FILTERED_ARGS + FILTERED_ARGS=$(echo "$@" | sed "s/password='\([^']*\)'/password='***'/g") + log_debug Running: mbimcli -p -d "/dev/$CDC_DEV" "$FILTERED_ARGS" + fi + local OUTPUT + local RV + OUTPUT="$(timeout -s INT -k 5 "$CMD_TIMEOUT" \ + mbimcli -p -d "/dev/$CDC_DEV" "$@" 2>/tmp/mbimcli.stderr)" + RV=$? + if [ "$VERBOSE" = "true" ]; then + log_debug mbimcli output: "$OUTPUT" + [ -s /tmp/mbimcli.stderr ] && log_debug mbimcli error: "$(cat /tmp/mbimcli.stderr)" + log_debug mbimcli RV: "$RV" + fi + echo "$OUTPUT" + cat /tmp/mbimcli.stderr 1>&2 + return $RV } mbim_get_packet_stats() { @@ -23,7 +41,15 @@ mbim_get_packet_stats() { } mbim_get_signal_info() { - local INFO="$(mbim --query-signal-state)" + local INFO + # Prefer signal info obtained using QMI-over-MBIM - it provides all metrics, + # not just RSSI. + if INFO="$(qmi_get_signal_info)"; then + echo "$INFO" + return 0 + fi + INFO="$(mbim --query-signal-state)" + local RV=$? local RSSI=$(parse_modem_attr "$INFO" "RSSI \[0-31,99\]") if [ "${RSSI:-99}" -eq 99 ]; then RSSI="$UNAVAIL_SIGNAL_METRIC" @@ -36,23 +62,35 @@ mbim_get_signal_info() { "$(json_attr rsrq "$UNAVAIL_SIGNAL_METRIC")" \ "$(json_attr rsrp "$UNAVAIL_SIGNAL_METRIC")" \ "$(json_attr snr "$UNAVAIL_SIGNAL_METRIC")" + return $RV } -# mbim_get_op_mode returns one of: "" (aka unspecified), "online", "online-and-connected", "radio-off", "offline", "unrecognized" -mbim_get_op_mode() { +mbim_is_radio_off() { local RF_STATE="$(mbim --query-radio-state)" local HW_RF_STATE="$(parse_modem_attr "$RF_STATE" "Hardware radio state")" local SW_RF_STATE="$(parse_modem_attr "$RF_STATE" "Software radio state")" if [ "$HW_RF_STATE" = "off" ] || [ "$SW_RF_STATE" = "off" ]; then + return 0 + fi + return 1 +} + +# mbim_get_op_mode returns one of: "" (aka unspecified), "online", "online-and-connected", "radio-off", "offline", "unrecognized" +mbim_get_op_mode() { + if mbim_is_radio_off; then echo "radio-off" return fi - if mbim --query-packet-service-state | grep -q "Packet service state:\s*'attached'"; then - echo "online-and-connected" - else - # FIXME XXX detect offline state - echo "online" + if mbim_get_registration_state | grep -qvE '(home|roaming|partner)'; then + echo "offline" + return + fi + if [ "$(mbim_get_packet_service_state)" = "attached" ] && \ + [ "$(mbim_get_connection_state)" = "activated" ]; then + echo "online-and-connected" + return fi + echo "online" } mbim_get_imei() { @@ -67,11 +105,54 @@ mbim_get_modem_revision() { parse_modem_attr "$(mbim --query-device-caps)" "Firmware info" } +mbim_get_modem_manufacturer() { + # Not available with pure MBIM, try QMI-over-MBIM. + qmi_get_modem_manufacturer +} + +mbim_get_current_provider() { + local INFO="$(mbim --query-registration-state)" + local REGSTATE="$(parse_modem_attr "$INFO" "Register state")" + if echo "$REGSTATE" | grep -qvE '(home|roaming|partner|denied)'; then + echo "{}" + return 1 + fi + local PLMN="$(parse_modem_attr "$INFO" "Provider ID")" + local DESCRIPTION="$(parse_modem_attr "$INFO" "Provider name")" + local ROAMING="false" + # Convert PLMN to dash-separated format (xxx-yy). + PLMN="$(echo "$PLMN" | cut -c1-3)-$(echo "$PLMN" | cut -c4-)" + if [ "$REGSTATE" = "roaming" ]; then + ROAMING="true" + fi + local FORBIDDEN="false" + if [ "$REGSTATE" = "denied" ]; then + FORBIDDEN="true" + fi + json_struct \ + "$(json_str_attr plmn "${PLMN}")" \ + "$(json_str_attr description "${DESCRIPTION}")" \ + "$(json_attr current-serving "true")" \ + "$(json_attr roaming "${ROAMING}")" \ + "$(json_attr forbidden "${FORBIDDEN}")" +} + mbim_get_providers() { local PROVIDERS - if ! PROVIDERS="$(mbim --query-visible-providers)"; then + if mbim_is_radio_off; then + # With radio off, an attempt to list visible providers would fail and log error. echo "[]" - return 1 + return + fi + if ! PROVIDERS="$(CMD_TIMEOUT=120 mbim --query-visible-providers)"; then + # Alternative to listing all providers is to return info at least + # for the current provider. + if SERVING="$(mbim_get_current_provider)"; then + printf "[%b]" "$SERVING" + else + echo "[]" + fi + return fi echo "$PROVIDERS" | awk ' BEGIN{RS="Provider [[0-9]+]:"; FS="\n"; print "["} @@ -91,9 +172,11 @@ mbim_get_providers() { if ($i~/State:/) { current="false" roaming="false" + forbidden="false" if ($i~/registered/) current="true" if ($i~/roaming/) roaming="true" - kv="\"current-serving\":" current ",\"roaming\":" roaming + if ($i~/forbidden/) forbidden="true" + kv="\"current-serving\":" current ",\"roaming\":" roaming ",\"forbidden\":" forbidden } if (kv) { print sep_inner kv @@ -106,20 +189,98 @@ mbim_get_providers() { END{print "]"}' | jq -c "unique" } +mbim_get_currently_used_rats() { + # Only available with QMI or QMI-over-MBIM. + qmi_get_currently_used_rats +} + +mbim_get_all_sim_slots() { + local CAPS="$(mbim --ms-query-sys-caps)" + local COUNT="$(parse_modem_attr "$CAPS" "Number of slots")" + COUNT="${COUNT:-1}" # Assume there is only one slot. + local IDX=1 + while [ "$IDX" -le "$COUNT" ]; do + echo "$IDX" + IDX="$((IDX + 1))" + done +} + +mbim_get_active_sim_slot() { + # XXX "state-empty" seems to be reported for absent SIM card even when slot + # is inactivate (instead of "state-off-empty" which would be correct). + local EMPTY_SLOT="" + local SLOT="" + for SLOT in $(mbim_get_all_sim_slots); do + local STATUS="$(mbim --ms-query-slot-info-status "$((SLOT-1))")" + local STATE="$(parse_modem_attr "$STATUS" "Slot '$((SLOT-1))'")" + case "$STATE" in + ""|"state-unknown"|"state-off-empty"|"state-off") + continue + ;; + "state-empty") + # Can't tell if the slot is activated. + EMPTY_SLOT="$SLOT" + continue + ;; + *) echo "$SLOT" && return + ;; + esac + done + # Try QMI-over-MBIM if supported and we failed to find activated slot with MBIM. + SLOT="$(qmi_get_active_sim_slot)" + [ -n "$SLOT" ] && echo "$SLOT" && return + # Assume that the SIM slot without card inserted is the activated one. + # With multiple empty SIM slots present this is not going to always give + # the correct answer. + [ -n "$EMPTY_SLOT" ] && echo "$EMPTY_SLOT" +} + mbim_get_sim_cards() { - # FIXME XXX Limited to a single SIM card - local SUBSCRIBER - if ! SUBSCRIBER="$(mbim --query-subscriber-ready-status)"; then - echo "[]" - return 1 - fi - local ICCID=$(parse_modem_attr "$SUBSCRIBER" "SIM ICCID") - # Remove trailing Fs that modem may add as a padding. - ICCID="$(echo "$ICCID" | tr -d "F")" - local IMSI="$(parse_modem_attr "$SUBSCRIBER" "Subscriber ID")" - local STATUS="$(parse_modem_attr "$SUBSCRIBER" "Ready state")" - SIM="$(json_struct "$(json_str_attr "iccid" "$ICCID")" "$(json_str_attr "imsi" "$IMSI")" "$(json_str_attr "status" "$STATUS")")\n" - printf "%b" "$SIM" | json_array + local SIMS + local SUBSCRIBER="$(mbim --query-subscriber-ready-status)" + local ACTIVE_SLOT="$(mbim_get_active_sim_slot)" + for SLOT in $(mbim_get_all_sim_slots); do + local ICCID="" + local IMSI="" + local ACTIVATED="false" + if [ "$SLOT" = "$ACTIVE_SLOT" ]; then + ACTIVATED="true" + # Get ICCID of the currently active SIM card. + # It is not supported (and likely not even possible) to obtain ICCIDs of SIM + # cards inside inactive slots. + ICCID=$(parse_modem_attr "$SUBSCRIBER" "SIM ICCID") + # Remove trailing Fs that modem may add as a padding. + ICCID="$(echo "$ICCID" | tr -d "F")" + IMSI="$(parse_modem_attr "$SUBSCRIBER" "Subscriber ID")" + fi + local SIM_STATE="unknown" + local STATUS="$(mbim --ms-query-slot-info-status "$((SLOT-1))")" + local SLOT_STATE="$(parse_modem_attr "$STATUS" "Slot '$((SLOT-1))'")" + case "$SLOT_STATE" in + "state-off-empty"|"state-empty") + # Takes precedence over "inactive". + SIM_STATE="absent" + ;; + "state-off") + SIM_STATE="inactive" + ;; + "state-active"|"state-active-esim") + # See mbim_get_subscriber_state for possible values. + SIM_STATE="$(parse_modem_attr "$SUBSCRIBER" "Ready state")" + ;; + *) # one of: state-error, state-not-ready, state-active-esim-no-profiles + SIM_STATE="${SLOT_STATE#state-}" + ;; + esac + local SIM_STATUS="$(json_struct \ + "$(json_attr slot-number "$SLOT")" \ + "$(json_attr slot-activated "$ACTIVATED")" \ + "$(json_str_attr iccid "$ICCID")" \ + "$(json_str_attr imsi "$IMSI")" \ + "$(json_str_attr state "$SIM_STATE")")" + SIMS="${SIMS}${SIM_STATUS}\n" + done + printf "%b" "$SIMS" | json_array } mbim_get_ip_settings() { @@ -135,66 +296,153 @@ mbim_get_ip_settings() { } mbim_start_network() { - echo "[$CDC_DEV] Starting network for APN ${APN}" - # NOTE that after --attach-packet-service we may end in a state - # where packet service is attached but WDS hasn't come up online - # just yet. We're blocking on WDS in wait_for_wds(). However, it - # may be useful to check --query-packet-service-state just in case. - mbim --attach-packet-service - sleep 10 - mbim --connect="apn='${APN}'" + log_debug "[$CDC_DEV] Starting network for APN ${APN}" + if ! mbim --attach-packet-service; then + # Maybe the modem was restarted to apply changed preferred PLMNs/RATs. + # Wait for subscriber initialization (again), then retry (but only once). + if ! mbim_wait_for_subscriber_init || ! mbim --attach-packet-service; then + log_error "Failed to attach to packet service" + return 1 + fi + fi + local ARGS="apn='${APN}'" + if [ -n "$USERNAME" ]; then + local PROTO + case "$AUTH_PROTO" in + "") PROTO="none" + ;; + "pap-and-chap") + # This option does not seem to be supported with mbimcli + log_error "unsupported authentication protocol: $AUTH_PROTO" + return 1 + ;; + *) PROTO="$AUTH_PROTO" + ;; + esac + ARGS="$ARGS,username='${USERNAME}',password='${PASSWORD}',auth='${PROTO}'" + fi + mbim --connect="$ARGS" } -mbim_wait_for_sim() { - echo "[$CDC_DEV] Waiting for SIM card to initialize" - local CMD="mbim --query-subscriber-ready-status | grep -q 'Ready state: .initialized.' && echo initialized" +mbim_switch_to_selected_slot() { + if [ -n "$SIM_SLOT" ] && [ "$SIM_SLOT" != "0" ]; then + if [ "$(mbim_get_active_sim_slot)" != "$SIM_SLOT" ]; then + if ! mbim --ms-set-device-slot-mappings="$((SIM_SLOT-1))"; then + log_error "Failed to switch to SIM slot $SIM_SLOT" + return 1 + fi + if ! wait_for 3 "$SIM_SLOT" mbim_get_active_sim_slot; then + log_error "Timeout waiting for modem to switch to SIM slot $SIM_SLOT" + return 1 + fi + # Give modem some time to update the SIM & Subscriber state. + sleep 3 + fi + fi +} + +# Possible values: not-initialized, initialized, sim-not-inserted, bad-sim, failure, +# not-activated, device-locked +mbim_get_subscriber_state() { + parse_modem_attr "$(mbim --query-subscriber-ready-status)" "Ready state" +} - if ! wait_for initialized "$CMD"; then - echo "Timeout waiting for SIM initialization" >&2 +mbim_wait_for_subscriber_init() { + # Wait as long as the state is not-activated or not-initialized. + local CMD="mbim_get_subscriber_state | grep -q '^not-' || echo done" + if ! wait_for 3 done "$CMD"; then + log_error "Timeout waiting for SIM initialization" return 1 fi + local STATE="$(mbim_get_subscriber_state)" + if [ "$STATE" = initialized ]; then + return 0 + fi + log_error "Subscriber is not initialized (state: $STATE)" + return 1 } -mbim_wait_for_wds() { - echo "[$CDC_DEV] Waiting for DATA services to connect" - # FIXME XXX there seems to be cases where this looks like connected - local CMD="mbim --query-connection-state | grep -q 'Activation state: .activated.' && echo connected" +mbim_wait_for_sim() { + # Switch to the correct SIM slot first. + if ! mbim_switch_to_selected_slot; then + return 1 + fi + log_debug "[$CDC_DEV] Waiting for SIM card to initialize" + mbim_wait_for_subscriber_init +} + +# Returns one of: "unknown", "activated", "activating", "deactivated", "deactivating". +mbim_get_connection_state() { + parse_modem_attr "$(mbim --query-connection-state)" "Activation state" +} - if ! wait_for connected "$CMD"; then - echo "Timeout waiting for DATA services to connect" >&2 +# Returns one of: "unknown", "attaching", "attached", "detaching", "detached". +mbim_get_packet_service_state() { + parse_modem_attr "$(mbim --query-packet-service-state)" "Packet service state" +} + +mbim_wait_for_wds() { + log_debug "[$CDC_DEV] Waiting for DATA services to connect" + if ! wait_for 5 attached mbim_get_packet_service_state; then + log_error "Timeout waiting for Packet service to attach" + return 1 + fi + if ! wait_for 5 activated mbim_get_connection_state; then + log_error "Timeout waiting for connection to activate" return 1 fi } +# Returns one of: "unknown", "deregistered", "searching", "home", "roaming", +# "partner" (registered in a preferred roaming network), "denied". +mbim_get_registration_state() { + parse_modem_attr "$(mbim --query-registration-state)" "Register state" +} + +mbim_get_registered_plmn() { + parse_modem_attr "$(mbim --query-registration-state)" "Provider ID" +} + +mbim_log_registration_debug_info() { + if [ "$VERBOSE" = "true" ]; then + # Output of these commands will be logged and can be used for debugging. + mbim --query-preferred-providers 2>&1 >/dev/null + mbim --query-signal-state 2>&1 >/dev/null + # If QMI-over-MBIM is available... + qmi_log_registration_debug_info + fi +} + mbim_wait_for_register() { - # Make sure we are registering with the right APN. - # Some LTE networks require explicit (and correct) APN for the registration/attach - # procedure (for the initial EPS bearer activation). - # Note that qmicli is able to apply this change even in the mbim mode. - # On the other hand, mbimcli does not yet provide command to manipulate with profiles. - local PROFILE="$(qmi --wds-get-default-profile-num=3gpp)" - local PROFILE_NUM="$(parse_modem_attr "$PROFILE" "Default profile number")" - qmi --wds-modify-profile="3gpp,${PROFILE_NUM},apn=${APN}" - - echo "[$CDC_DEV] Waiting for the device to register on the network" - local CMD="mbim --query-registration-state | grep -qE 'Register state:.*(home|roaming|partner)' && echo registered" - - if ! wait_for registered "$CMD"; then - echo "Timeout waiting for the device to register on the network" >&2 + # Configuring profile for the initial EPS bearer activation as well as selecting + # preferred RATs and providers is apparently only possible with QMI or QMI-over-MBIM. + qmi_set_registration_profile + qmi_set_preferred_plmns_and_rats + # Wait for the initial EPS Bearer activation. + log_debug "[$CDC_DEV] Waiting for the device to register on the network" + mbim_log_registration_debug_info + local CMD="mbim_get_registration_state | grep -qE '(home|roaming|partner|denied)' && echo done" + if ! wait_for 10 done "$CMD"; then + log_error "Timeout waiting for the device to register on the network" return 1 fi + if [ "$(mbim_get_registration_state)" != denied ]; then + log_debug "[$CDC_DEV] Registered on network $(mbim_get_registered_plmn)" + return 0 + fi + log_error "Network registration was denied" + return 1 } mbim_get_ip_address() { mbim --query-ip-configuration | jq -r .ipv4.ip } -mbim_wait_for_settings() { - echo "[$CDC_DEV] Waiting for IP configuration for the $IFACE interface" +mbim_wait_for_ip_config() { + log_debug "[$CDC_DEV] Waiting for IP configuration for the $IFACE interface" local CMD="mbim_get_ip_address | grep -q \"$IPV4_REGEXP\" && echo connected" - - if ! wait_for connected "$CMD"; then - echo "Timeout waiting for IP configuration for the $IFACE interface" >&2 + if ! wait_for 5 connected "$CMD"; then + log_error "Timeout waiting for IP configuration for the $IFACE interface" return 1 fi } @@ -206,10 +454,17 @@ mbim_stop_network() { mbim_toggle_rf() { if [ "$1" = "off" ]; then - echo "[$CDC_DEV] Disabling RF" + log_debug "[$CDC_DEV] Disabling RF" mbim --set-radio-state "off" else - echo "[$CDC_DEV] Enabling RF" + log_debug "[$CDC_DEV] Enabling RF" mbim --set-radio-state "on" fi } + +mbim_is_modem_responsive() { + # Try the NOOP method and to get device capabilities. + # If both calls are failing to return in 3 seconds, consider device as unresponsive. + ! CMD_TIMEOUT=3 mbim --noop && ! CMD_TIMEOUT=3 mbim --query-device-caps && return 1 + return 0 +} diff --git a/pkg/wwan/usr/bin/wwan-qmi.sh b/pkg/wwan/usr/bin/wwan-qmi.sh index 707f9c6f7b4..9c63b2cbda5 100644 --- a/pkg/wwan/usr/bin/wwan-qmi.sh +++ b/pkg/wwan/usr/bin/wwan-qmi.sh @@ -4,7 +4,25 @@ # shellcheck disable=SC2034 qmi() { - timeout -s INT -k 5 "$LTESTAT_TIMEOUT" qmicli -p -d "/dev/$CDC_DEV" "$@" + if [ "$VERBOSE" = "true" ]; then + # Do not log user password. + local FILTERED_ARGS + FILTERED_ARGS="$(echo "$@" | sed "s/password='\([^']*\)'/password='***'/g")" + log_debug Running: qmicli -p -d "/dev/$CDC_DEV" "$FILTERED_ARGS" + fi + local OUTPUT + local RV + OUTPUT="$(timeout -s INT -k 5 "$CMD_TIMEOUT" \ + qmicli -p -d "/dev/$CDC_DEV" "$@" 2>/tmp/qmicli.stderr)" + RV=$? + if [ "$VERBOSE" = "true" ]; then + log_debug qmicli output: "$OUTPUT" + [ -s /tmp/qmicli.stderr ] && log_debug qmicli error: "$(cat /tmp/qmicli.stderr)" + log_debug qmicli RV: "$RV" + fi + echo "$OUTPUT" + cat /tmp/qmicli.stderr 1>&2 + return $RV } qmi_get_packet_stats() { @@ -22,6 +40,7 @@ qmi_get_packet_stats() { qmi_get_signal_info() { local INFO="$(qmi --nas-get-signal-info)" + local RV=$? local RSSI="$(parse_modem_attr "$INFO" "RSSI" " dBm")" local RSRQ="$(parse_modem_attr "$INFO" "RSRQ" " dB")" local RSRP="$(parse_modem_attr "$INFO" "RSRP" " dBm")" @@ -36,18 +55,21 @@ qmi_get_signal_info() { "$(json_attr rsrq "${RSRQ:-$UNAVAIL_SIGNAL_METRIC}")" \ "$(json_attr rsrp "${RSRP:-$UNAVAIL_SIGNAL_METRIC}")" \ "$(json_attr snr "${SNR:-$UNAVAIL_SIGNAL_METRIC}")" + return $RV } -qmi_get_packet_state() { +# Possible states are: unknown, disconnected, connected, suspended, authenticating +qmi_get_connection_state() { qmi --wds-get-packet-service-status | sed -n "s/.*Connection status: '\(.*\)'/\1/p" } -# qmi_get_op_mode returns one of: "" (aka unspecified), "online", "online-and-connected", "radio-off", "offline", "unrecognized" +# qmi_get_op_mode returns one of: "" (aka unspecified), "online", "online-and-connected", +# "radio-off", "offline", "unrecognized" qmi_get_op_mode() { local OP_MODE="$(parse_modem_attr "$(qmi --dms-get-operating-mode)" "Mode")" case "$OP_MODE" in "online") - if [ "$(qmi_get_packet_state)" = "connected" ]; then + if [ "$(qmi_get_connection_state)" = "connected" ]; then echo "online-and-connected" else echo "online" @@ -74,43 +96,56 @@ qmi_get_modem_revision() { parse_modem_attr "$(qmi --dms-get-revision)" "Revision" } -qmi_get_serving_system() { +qmi_get_modem_manufacturer() { + parse_modem_attr "$(qmi --dms-get-manufacturer)" "Manufacturer" +} + +qmi_get_current_provider() { local INFO="$(qmi --nas-get-serving-system)" local REGSTATE="$(parse_modem_attr "$INFO" "Registration state")" - if [ "$REGSTATE" != "registered" ]; then + if [ "$REGSTATE" != "registered" ] && [ "$REGSTATE" != "registration-denied" ]; then + echo "{}" return 1 fi local MCC="$(parse_modem_attr "$INFO" "MCC")" local MNC="$(parse_modem_attr "$INFO" "MNC")" - local DESCRIPTION="$(parse_modem_attr "$INFO" "Description")" - local ROAMING="$(parse_modem_attr "$INFO" "Roaming status")" local PLMN="$(printf "%03d-%02d" "$MCC" "$MNC" 2>/dev/null)" - if [ "$ROAMING" = "on" ]; then - ROAMING="true" - else - ROAMING="false" - fi + local DESCRIPTION="$(parse_modem_attr "$INFO" "Description")" + local ROAMING="$(parse_modem_attr "$INFO" "Roaming status" | onoff_to_bool)" + local FORBIDDEN="$(parse_modem_attr "$INFO" "Forbidden" | yesno_to_bool)" json_struct \ "$(json_str_attr plmn "${PLMN}")" \ "$(json_str_attr description "${DESCRIPTION}")" \ "$(json_attr current-serving "true")" \ - "$(json_attr roaming "${ROAMING}")" + "$(json_attr roaming "${ROAMING}")" \ + "$(json_attr forbidden "${FORBIDDEN}")" } qmi_get_providers() { local PROVIDERS - if ! PROVIDERS="$(qmi --nas-network-scan)"; then + if [ "$(qmi_get_op_mode)" = "radio-off" ]; then + # With radio off, an attempt to list visible providers would fail and log error. + echo "[]" + return + fi + if ! PROVIDERS="$(CMD_TIMEOUT=120 qmi --nas-network-scan)"; then # Alternative to listing all providers is to return info at least # for the current provider. - SERVING="$(qmi_get_serving_system)" - printf "[%b]" "$SERVING" + if SERVING="$(qmi_get_current_provider)"; then + printf "[%b]" "$SERVING" + else + echo "[]" + fi return fi if ! echo "$PROVIDERS" | grep -q "Network \[[0-9]\+\]"; then # Network scan was most likely aborted with output: # Network scan result: abort - SERVING="$(qmi_get_serving_system)" - printf "[%b]" "$SERVING" + if SERVING="$(qmi_get_current_provider)"; then + printf "[%b]" "$SERVING" + else + echo "[]" + fi return fi echo "$PROVIDERS" | awk ' @@ -134,9 +169,11 @@ qmi_get_providers() { if ($i~/Status:/) { current="false" roaming="false" + forbidden="true" if ($i~/current-serving/) current="true" if ($i~/roaming/) roaming="true" - kv="\"current-serving\":" current ",\"roaming\":" roaming + if ($i~/not-forbidden/) forbidden="false" + kv="\"current-serving\":" current ",\"roaming\":" roaming ",\"forbidden\":" forbidden } if (kv) { print sep_inner kv @@ -152,6 +189,116 @@ qmi_get_providers() { END{print "]"}' | jq -c "unique" } +qmi_get_currently_used_rats() { + local INFO + if ! INFO="$(qmi --nas-get-serving-system)"; then + echo "[]" + return + fi + echo "$INFO" | awk ' + /Radio interfaces:/ { + getline + while ($1 ~ /\[[0-9]+\]:/) { + gsub(/'\''/, "", $2) + if ($2 != "none") { + rats = rats "\"" $2 "\"," + } + getline + } + } + END { + sub(/,$/, "", rats) + printf "[%s]", rats + }' +} + +qmi_get_all_sim_slots() { + local STATUS + if ! STATUS="$(qmi --uim-get-slot-status)"; then + # if uim-get-slot-status fails, then the modem has most likely only one slot. + echo "1" + return 0 + fi + echo "$STATUS" | awk ' + /Physical slot/ { slot_index = gensub(/([0-9]+).*/, "\\1", 1, $3); print slot_index }' +} + +qmi_get_active_sim_slot() { + local STATUS + if ! STATUS="$(qmi --uim-get-slot-status)"; then + # if uim-get-slot-status fails, then the modem has most likely only one slot + # (and it cannot be deactivated). + echo "1" + return 0 + fi + local SLOT="$(echo "$STATUS" | awk ' + /Physical slot/ { slot_index = gensub(/([0-9]+).*/, "\\1", 1, $3) } + /Slot status: active/ { print slot_index }')" + local SLOT="${SLOT:-1}" + echo "$SLOT" +} + +qmi_check_sim_card_presence() { + local SLOT="$1" + local STATUS + local STATE + if STATUS="$(qmi --uim-get-card-status)"; then + STATE="$(echo "$STATUS" | awk ' + /^Slot \[[0-9]+\]:/ { + slot_index = substr($2, 2, length($2) - 3) + } + /Card state:/ && slot_index == "'$SLOT'" { + gsub(/'\''/, "", $3) + print $3 + }')" + else + # Card status not available, try to get slot status. + STATUS="$(qmi --uim-get-slot-status)" + STATE="$(echo "$STATUS" | awk ' + /Physical slot/ { slot_index = $3 } + /Card status:/ && slot_index == "'$SLOT':" { print $3 }')" + fi + [ "$STATE" = "present" ] && return 0 || return 1 +} + +qmi_get_sim_state() { + local SLOT="$1" + local ACTIVE_SLOT="$(qmi_get_active_sim_slot)" + if [ -z "$SLOT" ]; then + SLOT="$ACTIVE_SLOT" + fi + if ! qmi_check_sim_card_presence "$SLOT"; then + echo "absent" + return + fi + if [ "$SLOT" != "$ACTIVE_SLOT" ]; then + echo "inactive" + return + fi + local STATUS="$(qmi --uim-get-card-status)" + # Possible values obtained below: unknown, detected, pin1-or-upin-pin-required, + # puk1-or-upin-puk-required, check-personalization-state, pin1-blocked, illegal, ready + local STATE="$(echo "$STATUS" | awk ' + /\s*Primary GW/ { + slot = gensub(/.*slot \x27([0-9]+)\x27.*/, "\\1", 1) + app = gensub(/.*application \x27([0-9]+)\x27.*/, "\\1", 1) + } + /^Slot/ { + current_slot = gensub(/^Slot \[([0-9]+)\].*/, "\\1", 1) + } + /\s*Application \[[0-9]+\]:/ { + current_app = gensub(/\s*Application \[([0-9]+)\].*/, "\\1", 1) + } + /\s*Application state:/ && slot == current_slot && app == current_app { + gsub(/\x27/, "", $3); print $3 + }')" + STATE="${STATE:-"unknown"}" + echo "$STATE" +} + +# Get ICCID of the currently active SIM card. +# It is not supported (and likely not even possible) to obtain ICCIDs of SIM +# cards inside inactive slots. qmi_get_sim_iccid() { local OUTPUT # Get ICCID from User Identity Module (UIM). @@ -199,19 +346,27 @@ qmi_get_sim_imsi() { } qmi_get_sim_cards() { - # FIXME XXX Limited to a single SIM card - if ! ICCID="$(qmi_get_sim_iccid)"; then - echo "[]" - return 1 - fi - if ! IMSI="$(qmi_get_sim_imsi)"; then - echo "[]" - return 1 - fi - # Don't error out if this is empty - local STATUS="$(qmi_get_sim_status)" - local SIM="$(json_struct "$(json_str_attr "iccid" "$ICCID")" "$(json_str_attr "imsi" "$IMSI")" "$(json_str_attr "status" "$STATUS")")\n" - printf "%b" "$SIM" | json_array + local SIMS + local ACTIVE_SLOT="$(qmi_get_active_sim_slot)" + for SLOT in $(qmi_get_all_sim_slots); do + local ICCID="" + local IMSI="" + local STATE="$(qmi_get_sim_state "$SLOT")" + local ACTIVATED="false" + if [ "$SLOT" = "$ACTIVE_SLOT" ]; then + ACTIVATED="true" + ICCID="$(qmi_get_sim_iccid)" + IMSI="$(qmi_get_sim_imsi)" + fi + local SIM_STATUS="$(json_struct \ + "$(json_attr slot-number "$SLOT")" \ + "$(json_attr slot-activated "$ACTIVATED")" \ + "$(json_str_attr iccid "$ICCID")" \ + "$(json_str_attr imsi "$IMSI")" \ + "$(json_str_attr state "$STATE")")" + SIMS="${SIMS}${SIM_STATUS}\n" + done + printf "%b" "$SIMS" | json_array } qmi_get_ip_settings() { @@ -227,99 +382,184 @@ qmi_get_ip_settings() { } qmi_start_network() { - echo "[$CDC_DEV] Starting network for APN ${APN}" + log_debug "[$CDC_DEV] Starting network for APN ${APN}" ip link set "$IFACE" down echo Y > "/sys/class/net/$IFACE/qmi/raw_ip" ip link set "$IFACE" up - qmi --wds-reset - if ! OUTPUT="$(qmi --wds-start-network="ip-type=4,apn=${APN}" --client-no-release-cid)"; then + local ARGS="ip-type=4,apn='${APN}'" + if [ -n "$USERNAME" ]; then + local PROTO + case "$AUTH_PROTO" in + "") PROTO="none" + ;; + "pap-and-chap") PROTO="both" + ;; + *) PROTO="$AUTH_PROTO" + ;; + esac + ARGS="$ARGS,username='${USERNAME}',password='${PASSWORD}',auth='${PROTO}'" + fi + if ! OUTPUT="$(qmi --wds-start-network="$ARGS" --client-no-release-cid)"; then return 1 fi - parse_modem_attr "$OUTPUT" "Packet data handle" | mbus_publish "pdh_$IFACE" parse_modem_attr "$OUTPUT" "CID" | mbus_publish "cid_$IFACE" } -qmi_get_sim_status() { - # Get full state... - local STATE="$(qmi --uim-get-card-status)" - # The Primary GW stores which application is ready, if any... - local SLOT_AND_APP="$(parse_modem_attr "$STATE" "Primary GW")" - # Ensure format of the Primary GW... - if ! echo "$SLOT_AND_APP" | grep -q slot; then - echo "not-ready" - return 1 - fi - # Ensure app and slot are numbers... - local SLOT="$(echo "$SLOT_AND_APP" | sed 's/^.*slot \([0-9]*\),.*$/\1/')" - if ! echo "$SLOT" | grep -Eq "^[0-9]+$"; then - echo "not-ready" - return 1 - fi - local APP="$(echo "$SLOT_AND_APP" | sed 's/^.*slot .* application \([0-9]*\)$/\1/')" - if ! echo "$APP" | grep -Eq "^[0-9]+$"; then - echo "not-ready" - return 1 +qmi_switch_to_selected_slot() { + if [ -n "$SIM_SLOT" ] && [ "$SIM_SLOT" != "0" ]; then + if [ "$(qmi_get_active_sim_slot)" != "$SIM_SLOT" ]; then + if ! qmi --uim-switch-slot="$SIM_SLOT"; then + log_error "Failed to switch to SIM slot $SIM_SLOT" + return 1 + fi + if ! wait_for 3 "$SIM_SLOT" qmi_get_active_sim_slot; then + log_error "Timeout waiting for modem to switch to SIM slot $SIM_SLOT" + return 1 + fi + # Give modem some time to update the SIM state. + sleep 3 + fi fi - # Print only the requested application and print the Application state. - # This works by printing all lines between the desired slot and the next slot (if there is one), - # and then printing all lines between the desired application and the next one. Once we have - # printed the lines containing only the desired application, we parse the Application state from it. - parse_modem_attr "$(echo "$STATE" | sed -n "/Slot \[$SLOT\]/,/Slot \[/p" | sed -n "/Application \[$APP\]/,/Application \[/p")" "Application state" } qmi_wait_for_sim() { - echo "[$CDC_DEV] Waiting for SIM card to initialize" - local CMD="qmi_get_sim_status" - - if ! wait_for ready "$CMD"; then - echo "Timeout waiting for SIM initialization" >&2 + # Switch to the correct SIM slot first. + if ! qmi_switch_to_selected_slot; then return 1 fi + log_debug "[$CDC_DEV] Waiting for SIM card to initialize" + # Do not wait if SIM is blocked or absent. + local CMD="qmi_get_sim_state | grep -qE '(ready|absent|required|blocked|illegal)' && echo done" + if ! wait_for 3 done "$CMD"; then + log_error "Timeout waiting for SIM initialization" + return 1 + fi + local STATE="$(qmi_get_sim_state)" + if [ "$STATE" = ready ]; then + return 0 + fi + log_error "SIM card is not ready (state: $STATE)" + return 1 } qmi_wait_for_wds() { - echo "[$CDC_DEV] Waiting for DATA services to connect" - local CMD="qmi_get_packet_state" - - if ! wait_for connected "$CMD"; then - echo "Timeout waiting for DATA services to connect" >&2 + log_debug "[$CDC_DEV] Waiting for DATA services to connect" + if ! wait_for 5 connected qmi_get_connection_state; then + log_error "Timeout waiting for DATA services to connect" return 1 fi } -qmi_get_registration_status() { +# Possible states: not-registered, registered, not-registered-searching, registration-denied +qmi_get_registration_state() { parse_modem_attr "$(qmi --nas-get-serving-system)" "Registration state" } -qmi_wait_for_register() { - # Make sure we are registering with the right APN. - # Some LTE networks require explicit (and correct) APN for the registration/attach - # procedure (for the initial EPS bearer activation). +qmi_get_registered_plmn() { + local INFO="$(qmi --nas-get-serving-system)" + local MCC="$(parse_modem_attr "$INFO" "MCC")" + local MNC="$(parse_modem_attr "$INFO" "MNC")" + printf "%03d-%02d" "$MCC" "$MNC" 2>/dev/null +} + +# Configure profile for the initial/default EPS bearer activation. +qmi_set_registration_profile() { local PROFILE="$(qmi --wds-get-default-profile-num=3gpp)" local PROFILE_NUM="$(parse_modem_attr "$PROFILE" "Default profile number")" - qmi --wds-modify-profile="3gpp,${PROFILE_NUM},apn=${APN}" + local NO_ROAMING="$(echo "$FORBID_ROAMING" | bool_to_yesno)" + local ARGS="3gpp,${PROFILE_NUM},apn=${APN},pdp-type=ipv4,no-roaming=${NO_ROAMING}" + qmi --wds-modify-profile="$ARGS" +} - echo "[$CDC_DEV] Waiting for the device to register on the network" - local CMD="qmi_get_registration_status" +# TODO: preferred PLMNs do not work +# - nas-set-preferred-networks returns UimUninitialized +qmi_set_preferred_plmns_and_rats() { + if [ -n "$PREFERRED_RATS" ] || [ -n "$PREFERRED_PLMNS" ]; then + local RESET_MODEM=n + if [ -n "$PREFERRED_RATS" ]; then + local PREV_PREF="$(qmi --nas-get-system-selection-preference)" + local PREF_RATS=$(echo "$PREFERRED_RATS" | tr -d "[]\"" | tr "," "|") + if qmi --nas-set-system-selection-preference="$PREF_RATS"; then + local NEW_PREF="$(qmi --nas-get-system-selection-preference)" + if [ "$PREV_PREF" != "$NEW_PREF" ]; then + RESET_MODEM=y + fi + fi + fi + if [ -n "$PREFERRED_PLMNS" ]; then + local PREV_PREF="$(qmi --nas-get-preferred-networks)" + # Add "all" after each PLMN - it means we accept any access technology. + local PREF_NETS="$(echo "$PREFERRED_PLMNS" | tr -d "[]\"-" | awk -F"," ' + { + for (i=1; i<=NF; i++) { + printf "%s,all", $i + if (i < NF) printf "," + } + }')" + if qmi --nas-set-preferred-networks="$PREF_NETS"; then + local NEW_PREF="$(qmi --nas-get-preferred-networks)" + if [ "$PREV_PREF" != "$NEW_PREF" ]; then + RESET_MODEM=y + fi + fi + fi + # Reset modem to trigger re-registration. + if [ "$RESET_MODEM" = y ]; then + qmi --dms-set-operating-mode=offline + qmi --dms-set-operating-mode=reset + CMD="qmi --dms-get-operating-mode | grep -q Mode && echo running" + if ! wait_for 10 running "$CMD" 2>/dev/null; then + log_error "Timeout waiting for modem to reset" + return 1 + fi + fi + fi +} - if ! wait_for registered "$CMD"; then - echo "Timeout waiting for the device to register on the network" >&2 +qmi_log_registration_debug_info() { + if [ "$VERBOSE" = "true" ]; then + # Output of these commands will be logged and can be used for debugging. + qmi --nas-get-system-selection-preference 2>&1 >/dev/null + qmi --nas-get-preferred-networks 2>&1 >/dev/null + qmi --nas-get-cell-location-info 2>&1 >/dev/null + qmi --nas-get-signal-strength 2>&1 >/dev/null + qmi --nas-get-rf-band-info 2>&1 >/dev/null + fi +} + +qmi_wait_for_register() { + # Apply config for the initial EPS Bearer activation. + qmi_set_registration_profile + qmi_set_preferred_plmns_and_rats + # Wait for the initial EPS Bearer activation. + log_debug "[$CDC_DEV] Waiting for the device to register on the network" + qmi_log_registration_debug_info + # Wait until registered or getting denied. + local CMD="qmi_get_registration_state | grep -Eq '(^registered|denied)' && echo done" + if ! wait_for 10 done "$CMD"; then + log_error "Timeout waiting for the device to register on the network" return 1 fi + STATE="$(qmi_get_registration_state)" + if [ "$STATE" = registered ]; then + log_debug "[$CDC_DEV] Registered on network $(qmi_get_registered_plmn)" + return 0 + fi + log_error "Network registration failed (state: $STATE)" + return 1 } qmi_get_ip_address() { parse_modem_attr "$(qmi --wds-get-current-settings)" "IPv4 address" } -qmi_wait_for_settings() { - echo "[$CDC_DEV] Waiting for IP configuration for the $IFACE interface" +qmi_wait_for_ip_config() { + log_debug "[$CDC_DEV] Waiting for IP configuration for the $IFACE interface" local CMD="qmi_get_ip_address | grep -q \"$IPV4_REGEXP\" && echo connected" - - if ! wait_for connected "$CMD"; then - echo "Timeout waiting for IP configuration for the $IFACE interface" >&2 + if ! wait_for 5 connected "$CMD"; then + log_error "Timeout waiting for IP configuration for the $IFACE interface" return 1 fi } @@ -327,21 +567,31 @@ qmi_wait_for_settings() { qmi_stop_network() { local PDH="$(cat "${BBS}/pdh_${IFACE}.json" 2>/dev/null)" local CID="$(cat "${BBS}/cid_${IFACE}.json" 2>/dev/null)" - if ! qmi --wds-stop-network="$PDH" --client-cid="$CID"; then # If qmicli failed to stop the network, reset operating mode of the modem. - qmi --dms-set-operating-mode=low-power - sleep 1 - qmi --dms-set-operating-mode=online + if [ "$(qmi_get_op_mode)" = "online-and-connected" ]; then + qmi --dms-set-operating-mode=low-power + sleep 1 + qmi --dms-set-operating-mode=online + fi fi + # Never return error from here, let reconnection attempt to continue. + return 0 } qmi_toggle_rf() { if [ "$1" = "off" ]; then - echo "[$CDC_DEV] Disabling RF" + log_debug "[$CDC_DEV] Disabling RF" qmi --dms-set-operating-mode=persistent-low-power else - echo "[$CDC_DEV] Enabling RF" + log_debug "[$CDC_DEV] Enabling RF" qmi --dms-set-operating-mode=online fi } + +qmi_is_modem_responsive() { + # Try NOOP methods from DMS and NAS services. + # If both are failing to return within 3secs, consider device as unresponsive. + ! CMD_TIMEOUT=3 qmi --dms-noop && ! CMD_TIMEOUT=3 qmi --nas-noop && return 1 + return 0 +}