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 +}