{
  liminix
, usb-modeswitch
, ppp
, lib
, svc
, uevent-watch
}:
{ apn, username, password, authType }:
let
  inherit (liminix.services) bundle longrun oneshot;
  authTypeNum = if authType == "pap" then "1" else "2";
  chat = lib.escapeShellArgs [
    # Your usb modem thing might present as a tty that you run PPP
    # over, or as a network device ("ndis" or "ncm"). The latter
    # kind is to be preferred, at least in principle, because it's
    # faster.  This initialization sequence works for the Huawei
    # E3372, and took much swearing: the error messages are *awful*
    "" "AT"
    "OK" "ATZ"
    # create PDP context
    "OK" "AT+CGDCONT=1,\"IP\",\"${apn}\""
    # activate PDP context
    "OK"  "AT+CGACT=1,1"
    # setup username and password per requirements of sim provider.
    # (caret is special to chat, so needs escaping in AT commands)
    "OK"  "AT\\^AUTHDATA=1,${authTypeNum},\"\",\"${password}\",\"${username}\""
    # start the thing (I am choosing to read this as "NDIS DialUP")
    "OK" "AT\\^NDISDUP=1,1"
    "OK"
  ];
  modeswitch = oneshot rec {
    name = "modem-modeswitch";
    controller = (svc.uevent-rule.build {
      serviceName = name;
      terms = { devtype = "usb_device"; product = "12d1/14fe/102"; };
    });
    up = ''
      ${usb-modeswitch}/bin/usb_modeswitch -v 12d1 -p 14fe --huawei-new-mode
    '';
  };
  atz = oneshot rec {
    name = "modem-atz";
    dependencies = [ modeswitch ];
    controller = (svc.uevent-rule.build {
      serviceName = name;
      terms = {
        subsystem = "tty";
        attrs = {
          idVendor = "12d1";
          idProduct = "1506";
        };
      };
      symlink = "/dev/modem";
    });
    up = ''
      ls -l /dev/modem
      test -L /dev/modem || exit 1
      ${ppp}/bin/chat -s -v ${chat}  0<>/dev/modem 1>&0
    '';
    down = "${ppp}/bin/chat -v '' ATZ OK  0<>/dev/modem 1>&0";
  };

in svc.network.link.build {
  ifname = "wwan0";
  dependencies = [ atz ];
}