Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wwan: Do not change initial bearer config and properly clear unused default bearers #4401

Merged
merged 2 commits into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions pkg/wwan/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,92 @@ at!reset
# Press CTRL-A, then CTRL-X to exit
```

### Capturing control-plane traffic

[QCSuper](https://github.com/P1sec/QCSuper) allows to capture raw 2G/3G/4G (and for certain
models also 5G) control-plane radio frames from Qualcomm-based modems.

It is necessary to first enable the modem Diag port using an AT command:

```console
eve enter wwan
# Use "mmcli -L" to find out the index of your modem.
mmcli -m <modem-index> --command="AT$QCDMG"
```

If this command fails, then your modem is either not Qualcomm-based or the Diag port is not
available and this pcap method will not work.

The Diag port should be accessible as a serial-over-USB device at `/dev/ttyUSB{0-9}`.
The modem may expose multiple such devices, with one or more dedicated to AT commands,
another for streaming GNSS location data, and one specifically reserved for Diagnostics/Debugging
(aka Diag).

Using `mmcli -m <modem-index>` you may find out the role of each of these ports.
However, the Diag port can be reported as "ignored":

```console
System | device: /sys/devices/pci0000:00/0000:00:14.0/usb1/1-5
| physdev: /sys/devices/pci0000:00/0000:00:14.0/usb1/1-5
| drivers: option, qmi_wwan
| plugin: quectel
| primary port: cdc-wdm0
| ports: cdc-wdm0 (qmi), ttyUSB0 (ignored), ttyUSB1 (gps),
| ttyUSB2 (at), ttyUSB3 (at), wwan0 (net)
```

The QCSuper tool requires python and some other dependencies which are not available
in EVE. However, we can relay access to the `/dev/ttyUSB{0-9}` device over the network
using socat and run QCSuper from another computer with python installed.

On EVE, execute:

```console
# Open access to port 12345 which we will use for relaying.
# After the packet capture is done, it is required to reboot the machine to bring back
# the firewall rules.
eve firewall drop
# socat is available in the debug container, no need to install anything.
# Replace "/dev/ttyUSB0" with the path to the Diag device of your modem.
eve enter debug
socat TCP-LISTEN:12345,reuseaddr,fork /dev/ttyUSB0,raw,echo=0
```

On another computer, install QCSuper with all the dependencies.
For example, if the computer is running Ubuntu, execute:

```console
sudo apt install python3-pip wireshark
sudo pip3 install --upgrade pyserial pyusb crcmod https://github.com/P1sec/pycrate/archive/master.zip
sudo pip3 install --upgrade qcsuper
# This is needed for wireshark to not complain about "permission denied":
sudo chmod +x /usr/bin/dumpcap
```

Then establish Diag port relay with:

```console
# Replace <eve-node-ip> with the IP address of your EVE node.
sudo socat PTY,link=/dev/virtualTTY0,raw,echo=0 TCP:<eve-node-ip>:12345
# This is needed to run qcsuper without root privileges, which in turn is needed
# to avoid Wireshark complaining:
sudo chmod 666 /dev/virtualTTY0
```

Start packet-capture with live Wireshark display using:

```console
# It is necessary to avoid having ModemManager running on your computer
# used for packet capture, otherwise qcsuper complains about possible interference
# and does not want to initiate the pcap.
sudo systemctl stop ModemManager.service
qcsuper --usb-modem /dev/virtualTTY0 --wireshark-live --reassemble-sibs --decrypt-nas --include-ip-traffic
```

After the packet capture is done, you can bring the ModemManager on your device
back with `sudo systemctl start ModemManager.service`, stop socat processes on both
ends and reboot the EVE edge-node to bring back the firewall rules.

## Enabling a new cellular modem

Go through the following steps (more detailed description below):
Expand Down
148 changes: 91 additions & 57 deletions pkg/wwan/mmagent/mmdbus/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,7 @@ func (c *Client) getModemStatus(modemObj dbus.BusObject) (
_ = getDBusProperty(c, modemObj, ModemPropertyBearers, &bearers)
for _, bearerPath := range bearers {
if !bearerPath.IsValid() {
c.log.Warnf("Bearer with an invalid dbus path: %s, skipping", bearerPath)
continue
}
bearerPaths = append(bearerPaths, string(bearerPath))
Expand Down Expand Up @@ -905,6 +906,7 @@ func (c *Client) getModemMetrics(
_ = getDBusProperty(c, modemObj, ModemPropertyBearers, &bearers)
for _, bearerPath := range bearers {
if !bearerPath.IsValid() {
c.log.Warnf("Bearer with an invalid dbus path: %s, skipping", bearerPath)
continue
}
bearerObj := c.conn.Object(MMInterface, bearerPath)
Expand Down Expand Up @@ -1208,6 +1210,8 @@ func (c *Client) Connect(
primarySIM = uint32(args.SIMSlot)
err := c.callDBusMethod(modemObj, ModemMethodSetPrimarySimSlot, nil, primarySIM)
if err != nil {
err = fmt.Errorf("failed to set SIM slot %d as primary for modem %s: %v",
primarySIM, modemPath, err)
return types.WwanIPSettings{}, err
}
err = c.waitForModemState(
Expand All @@ -1223,8 +1227,19 @@ func (c *Client) Connect(
// TODO: Not sure how to apply PreferredPLMNs with ModemManager.
err := c.setPreferredRATs(modemObj, args.PreferredRATs)
if err != nil {
err = fmt.Errorf("failed to set preferred RATs %v for modem %s: %v",
args.PreferredRATs, modemPath, err)
return types.WwanIPSettings{}, err
}
// Make sure that there are no previously created bearers still hanging
// around. Otherwise, we may get "interface-in-use-config-match" error
// from ModemManager.
err = c.deleteBearers(modemObj)
if err != nil {
// Just log as warning and continue with the connection attempt.
c.log.Warn(err)
err = nil
}
// Prepare connection settings.
connProps := make(map[string]interface{})
connProps["apn"] = args.APN
Expand Down Expand Up @@ -1252,42 +1267,63 @@ func (c *Client) Connect(
if err == nil {
return ipSettings, nil
}
origErr := err
// Try to fix failing connection attempt.
// First check if modem can even register.
changed, err := c.reconfigureEpsBearerIfNotRegistered(modemObj, connProps)
if changed && err == nil {
// Retry connection attempt with the same parameters applied also for the initial
// EPS bearer.
ipSettings, err = c.runSimpleConnect(modemObj, connProps)
if err == nil {
return ipSettings, nil
}
origErr := fmt.Errorf("failed to connect modem %s to APN %s: %v",
modemPath, args.APN, err)
// TODO: Allow the user to configure IP-type and PDP context for the initial EPS bearer.
// For now, we will be retrying connection attempts using all three IP types:
// ipv4, ipv6, ipv4v6. However, we will not be modifying the PDP context parameters
// for the initial EPS bearer until user is able to configure them.
//
// Some background for understanding: LTE connection consists of two IP bearers,
// the initial EPS bearer and the default EPS bearer. Device must first
// establish the initial bearer (which shows as transition from the "searching" to
// "registered" state) and then it connects to a default bearer (transition from
// "registered" to "connected"). Both bearers require PDP context settings (APN,
// ip-type, potentially username/password, etc.).
// Settings for the initial bearer are typically provided by the SIM card while
// settings for the default bearer are user-configured.
//
// It is not necessarily the case that the APNs for the initial and default bearers
// are the same. We used to make that assumption but this has led to cases where modem
// was failing registration because the APN for the initial bearer was wrong. It is
// better to let the SIM card provide the PDP context setting for the initial EPS bearer.
// Furthermore, once these settings are changed, there is no straightforward method to
// revert back to the SIM-provided configuration; for more details, see the discussion here:
// https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/issues/1490#note_2628804
//
// Users may only need to override SIM-provided settings in rather rare cases: either when
// the SIM card has incorrect configuration (we have seen this only once) or when the initial
// EPS bearer requires username/password authentication (also uncommon). Despite the rarity
eriknordmark marked this conversation as resolved.
Show resolved Hide resolved
// of these cases, these settings should be user-configurable.
// Please note, that the same enhancement was recently implemented in NetworkManager
// (used by Ubuntu and other major Linux distributions):
// https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/merge_requests/1915
err = c.deleteBearers(modemObj)
if err != nil {
// Just log as warning and continue with the next connection attempt for different
// ip-type.
c.log.Warn(err)
err = nil
}
// Next try IPv4 and IPv6 dual-stack.
// Try with dual-stack ip-type instead of IPv4 only.
connProps["ip-type"] = uint32(BearerIPFamilyIPv4v6)
_, err = c.reconfigureEpsBearerIfNotRegistered(modemObj, connProps)
ipSettings, err = c.runSimpleConnect(modemObj, connProps)
if err == nil {
ipSettings, err = c.runSimpleConnect(modemObj, connProps)
if err == nil {
return ipSettings, nil
}
return ipSettings, nil
}
err = c.deleteBearers(modemObj)
if err != nil {
// Just log as warning and continue with the next connection attempt for different
// ip-type.
c.log.Warn(err)
err = nil
}
// Make the final attempt with IPv6 only.
// This should be covered by IPv4v6 (network may return IPv6-only config
// in that case), but we make this attempt still just in case.
connProps["ip-type"] = uint32(BearerIPFamilyIPv6)
_, err = c.reconfigureEpsBearerIfNotRegistered(modemObj, connProps)
ipSettings, err = c.runSimpleConnect(modemObj, connProps)
if err == nil {
ipSettings, err = c.runSimpleConnect(modemObj, connProps)
if err == nil {
return ipSettings, nil
}
return ipSettings, nil
}
// Revert back the modem profile back to the preferred IPv4-only mode.
connProps["ip-type"] = uint32(BearerIPFamilyIPv4)
_, _ = c.reconfigureEpsBearerIfNotRegistered(modemObj, connProps)
// Return error from the first connection attempt (with IPv4-only).
return ipSettings, origErr
}

Expand Down Expand Up @@ -1348,33 +1384,6 @@ func (c *Client) runSimpleConnect(modemObj dbus.BusObject,
return ipSettings, err
}

func (c *Client) reconfigureEpsBearerIfNotRegistered(modemObj dbus.BusObject,
newSettings map[string]interface{}) (changedConfig bool, err error) {
var modemState int32
_ = getDBusProperty(c, modemObj, ModemPropertyState, &modemState)
if modemState >= ModemStateRegistered {
return false, nil
}
var currentSettings map[string]dbus.Variant
_ = getDBusProperty(c, modemObj, Modem3GPPPropertyInitialEpsBearer, &currentSettings)
maskedPasswd := interface{}("***")
maskedVariantPasswd := dbus.MakeVariant(maskedPasswd)
c.log.Warnf("Modem %s is failing to register, "+
"trying to apply settings %+v for the initial EPS bearer (previously: %+v)",
modemObj.Path(), maskPassword(newSettings, maskedPasswd),
maskPassword(currentSettings, maskedVariantPasswd))
err = c.callDBusMethod(modemObj, Modem3GPPMethodSetInitialEpsBearer, nil, newSettings)
if err != nil {
err = fmt.Errorf(
"failed to change initial EPS bearer settings for modem %s: %w",
modemObj.Path(), err)
c.log.Error(err)
return false, err
}
return true, c.waitForModemState(
modemObj, ModemStateRegistered, changeInitEPSBearerTimeout)
}

// maskPassword creates a copy of the original map with the "password" key's value masked
func maskPassword[Type any](data map[string]Type, maskWith Type) map[string]Type {
maskedData := make(map[string]Type)
Expand Down Expand Up @@ -1535,8 +1544,33 @@ func (c *Client) Disconnect(modemPath string) error {
c.mutex.Lock()
defer c.mutex.Unlock()
modemObj := c.conn.Object(MMInterface, dbus.ObjectPath(modemPath))
anyBearer := dbus.ObjectPath("/")
return c.callDBusMethod(modemObj, SimpleMethodDisconnect, nil, anyBearer)
return c.deleteBearers(modemObj)
}

// Delete all bearers which are associated with the given modem, except for
// the initial EPS bearer, which stays connected.
// This is used to disconnect the modem and to clean any bearers hanging
// after a previously failed connection request.
func (c *Client) deleteBearers(modemObj dbus.BusObject) error {
var bearers []dbus.ObjectPath
// This list does not include the initial EPS bearer details.
err := getDBusProperty(c, modemObj, ModemPropertyBearers, &bearers)
if err != nil {
return err
}
for _, bearerPath := range bearers {
if !bearerPath.IsValid() {
eriknordmark marked this conversation as resolved.
Show resolved Hide resolved
c.log.Warnf("Bearer with an invalid dbus path: %s, skipping", bearerPath)
continue
}
// If the bearer is currently active and providing packet data server, it will be
// disconnected and that packet data service will terminate.
err = c.callDBusMethod(modemObj, ModemMethodDeleteBearer, nil, bearerPath)
if err != nil {
return err
}
}
return nil
}

// ScanVisibleProviders runs a fairly long operation (takes around 1 minute!)
Expand Down
1 change: 1 addition & 0 deletions pkg/wwan/mmagent/mmdbus/mmapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const (
ModemMethodSetPowerState = ModemInterface + ".SetPowerState"
ModemMethodSetPrimarySimSlot = ModemInterface + ".SetPrimarySimSlot"
ModemMethodSetCurrentModes = ModemInterface + ".SetCurrentModes"
ModemMethodDeleteBearer = ModemInterface + ".DeleteBearer"
ModemPropertyModel = ModemInterface + ".Model"
ModemPropertyRevision = ModemInterface + ".Revision"
ModemPropertyManufacturer = ModemInterface + ".Manufacturer"
Expand Down
Loading