I've recently had the good fortune to come into possession of a reMarkable 2 E-Ink writing tablet. This device runs a modified version of Linux, and contains the following message in the copyrights and licenses information:

The General Public License version 3 and the Lesser General Public License version 3 also requires you as an end-user to be able to access your device to be able to modify the copyrighted software licensed under these licenses running on it.

To do so, this device acts as an USB ethernet device, and you can connect using the SSH protocol using the username 'root' and the password '<password>'.

As a result of this, there is a vibrant community of hacking for the remarkable. This blog post will walk through the hacks I've done on my device.

As implied in the GPLv3 Compliance statement, one can SSH into a reMarkable 2 using the Remote Network Driver Interface Specification (RNDIS) protocol for Ethernet over USB. The dropbear SSH server version v2019.78 shipped appears to only work with RSA keys, so if you're running openssh 8.8 or greater this needs added to your SSH configuration (either globally or under a specific host for the reMarkable):

PubkeyAcceptedKeyTypes +ssh-rsa
HostKeyAlgorithms +ssh-rsa

See also:

Once we're on here, we're presented with a friendly bash shell:

$ ssh root@10.11.99.1
reMarkable
╺━┓┏━╸┏━┓┏━┓   ┏━┓╻ ╻┏━╸┏━┓┏━┓
┏━┛┣╸ ┣┳┛┃ ┃   ┗━┓┃ ┃┃╺┓┣━┫┣┳┛
┗━╸┗━╸╹┗╸┗━┛   ┗━┛┗━┛┗━┛╹ ╹╹┗╸
reMarkable: ~/ ls
log.txt
reMarkable: ~/ ls /
bin             lib             postinst        tmp
boot            lost+found      proc            uboot-postinst
dev             media           run             usr
etc             mnt             sbin            var
home            opt             sys

I don't want to have to be constantly plugging my device in, though. We can SSH in over the local network, but dealing with firewalling networks or NAT punching across the Internet is a pain. Instead, we can install a VPN on the reMarkable 2. I installed tailscale, but another would work. The headscale OSS control server, or just plain WireGuard, would be something to look at.

Toltec is a package repository for the reMarkable. It leverages the Entware package repository and package manager for embedded devices. We can install toltec with:

reMarkable: ~/ wget http://toltec-dev.org/bootstrap
reMarkable: ~/ echo "04a28483286f88c5c7f39e352afb62adc57f6162a29fd7e124d832205bb0980e  bootstrap" | sha256sum -c && bash bootstrap

(I dislike running random curled bash scripts, but when in Rome...)

We can then install tailscale with toltec and set up a systemd service:

reMarkable: ~/ opkg install tailscale
reMarkable: ~/ cat "[Unit]
After=network.target
Description=Tailscale client daemon
StartLimitBurst=0
StartLimitIntervalSec=0
Wants=network.target
[Service]
Environment="HOME=/home/root"
ExecStart=/opt/bin/tailscaled --tun=userspace-networking --state=/opt/var/tailscaled.state
ExecStartPost=/opt/bin/tailscale up
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target" > /lib/systemd/system/tailscaled.service
reMarkable: ~/ systemctl enable --now tailscaled

NB --tun=userspace-networking is required as the reMarkable doesn't have modules for kernel space networking.

Now we can access our device pretty much anywhere we have an uplink:

$ ssh root@100.125.211.7
reMarkable
╺━┓┏━╸┏━┓┏━┓   ┏━┓╻ ╻┏━╸┏━┓┏━┓
┏━┛┣╸ ┣┳┛┃ ┃   ┗━┓┃ ┃┃╺┓┣━┫┣┳┛
┗━╸┗━╸╹┗╸┗━┛   ┗━┛┗━┛┗━┛╹ ╹╹┗╸
reMarkable: ~/

See:

Using the USB networking, there is a web interface for the reMarkable. This allows you to upload and download files from the device. However, as we've said, we want to be able to interact with this device without having to plug it in all the time. I tried to proxy this web interface remotely, but didn't meet with much success.

Going back to our SSH connection: we can SCP files over. But the reMarkable uses a custom directory layout and file formats in /home/root/.local/share/remarkable/xochitl. There is a script to copy PDF or EPUB files into this format, but it will not sync them back. We could look at using syncthing, or even version controlling using git, but this directory structure is still not the most useable format for us.

ReMarkable has a cloud service that would solve this problem for us. However, I don't particularly want to hand potentially sensitive documents over to this company, there are restrictions placed on the size and temporality of documents without a subscription (which I also would rather not pay for - being a price-sensitive PhD student), and I would be reliant on a provider that could cancel their service at any time.

Thankfully there is an open source clone of the reMarkable cloud, rmfakecloud. I deployed this on my existing NixOS server with:

    services.rmfakecloud = {
      enable = true;
      storageUrl = "https://${cfg.domain}";
      port = cfg.port;
      environmentFile = "${config.custom.secretsDir}/rmfakecloud.env";
      extraSettings = {
        RM_SMTP_SERVER = "mail.freumh.org:465";
        RM_SMTP_USERNAME = "misc@${domain}";
        RM_SMTP_FROM="remarkable@${domain}";
      };
    };

    mailserver.loginAccounts."misc@${domain}".aliases = [ "remarkable@${domain}" ];

    # nginx handles letsencrypt
    services.nginx = {
      enable = true;
      recommendedProxySettings = true;
      # to allow syncing
      # another option would just be opening a separate port for this
      clientMaxBodySize = "100M";
      virtualHosts."${cfg.domain}" = {
        forceSSL = true;
        enableACME = true;
        locations."/".proxyPass = ''
          http://localhost:${builtins.toString cfg.port}
        '';
      };
    };

    dns.records = [
      {
        name = "rmfakecloud";
        type = "CNAME";
        data = "vps";
      }
    ];

Which sets up the rmfakecloud service, a HTTP proxy, a mail alias, and DNS records1. See the full module at rmfakecloud.nix.

Note the clientMaxBodySize = "100M";. I can across an issue where my nginx proxy was limiting the maximum body size of a request to 10MB preventing the sync service from transferring blobs of around 30MB:

$ journalctl -u nginx
...
Dec 16 18:33:41 vps nginx[194956]: 2022/12/16 18:33:41 [error] 194956#194956: *521 client intended to send too large body: 32902724 bytes, client: 131.111.5.246, server: rmfakecloud.freumh.org, request: "PUT /blobstorage?blobid=d245bbed373b5f051c66c567201b5f06875f2714a509d6c69e0f759>
Dec 16 18:33:42 vps nginx[194956]: 2022/12/16 18:33:42 [error] 194956#194956: *521 client intended to send too large body: 32853572 bytes, client: 131.111.5.246, server: rmfakecloud.freumh.org, request: "PUT /blobstorage?blobid=d245bbed373b5f051c66c567201b5f06875f2714a509d6c69e0f759>
Dec 16 18:33:42 vps nginx[194956]: 2022/12/16 18:33:42 [error] 194956#194956: *521 client intended to send too large body: 32788036 bytes, client: 131.111.5.246, server: rmfakecloud.freumh.org, request: "PUT /blobstorage?blobid=d245bbed373b5f051c66c567201b5f06875f2714a509d6c69e0f759>
...

I set it to 100MB to be safe. Another option, as mentioned, would be to open the service on another port to avoid the proxy. However this may lead to firewalling issues.

Setting it up on the reMarkable was as simple as:

reMarkable: ~/ opkg install rmfakecloud-proxy
reMarkable: ~/ rmfakecloudctl set-upstream https://rmfakecloud.freumh.org
reMarkable: ~/ rmfakecloudctl enable

As described at rmfakecloud/docs/remarkable/setup.md.

This allows me to sync all my files to my server, and access them from my device when my reMarkable is offline. It also allows me to email documents with my own mailserver. It even supports handwriting recognition (offloaded to MyScript)

Xochitl is reMarkable's proprietary GUI for the device. It was xiocthl that imposed the directory layout from the previous section on us.

There are a wealth of other applications out there though:

All can be installed through toltec. However, we need some way to switch between them. There are 3 launchers for the reMarkable. All of them rely on remarkable2-framebuffer to render. This, in turn, relies on certain functions from Xochitl to do this. As Xochitl is a binary blob their locations need to be reverse-engineered, and likely change every update. This was the cause of an error I observed when trying to install a launcher:

Dec 16 23:39:06 reMarkable systemd[1]: Starting reMarkable 2 Framebuffer Server...
Dec 16 23:39:06 reMarkable xochitl[737]: STARTING RM2FB
Dec 16 23:39:06 reMarkable xochitl[737]: Missing address for function 'getInstance'
Dec 16 23:39:06 reMarkable xochitl[737]: PLEASE SEE https://github.com/ddvk/remarkable2-framebuffer/issues/18

Duly following instructions, I decompiled my version to find these addresses:

!20220929180236
version str 2.14.4.46
update addr 0x4c0a0c
updateType str QRect
create addr 0x4c3630
shutdown addr 0x4c35c8
wait addr 0x4c25d0
getInstance addr 0x4b7594

I could then install remux.

Hopefully this will prove useful to someone out there.


I've frequently found myself wanting to read long-form HTML documents from various web sources like blogs on my device. The simplest option here is to simply print said document to a PDF file with a browser, transfer it to the device, and read and annotate it like any other PDF. However, this is quite restrictive in terms of the reading format (it restricts the reading-time text size and pagination).

An alternative I found useful was to simply SCP the HTML file over and read it with KOReader, which has support for HTML. We're able to SCP the file as KOReader doesn't use the xiocthl file format. However, this means annotations aren't possible.

The final thing I tried was installing a full web browser on the reMarkable, for the hell of it. I use a fork of NetSurf installed with toltec, which works surprisingly well! I'm sticking with the first two options for now though: typing in NetSurf with a stylus is a pain.

I enabled a headscale control server for tailscale with the following NixOS module on my VPS:

{ pkgs, config, lib, ... }:

let
  cfg = config.eilean;
in {
  options.eilean.headscale = with lib; {
    enable = mkEnableOption "headscale";
    zone = mkOption {
      type = types.str;
      default = "${config.networking.domain}";
    };
    domain = mkOption {
      type = types.str;
      default = "headscale.${config.networking.domain}";
    };
  };

  config = lib.mkIf cfg.headscale.enable {
    services.headscale = {
      enable = true;
      # address = "127.0.0.1";
      port = 10000;
      serverUrl = "https://${cfg.headscale.domain}";
      dns = {
        # magicDns = true;
        nameservers = config.networking.nameservers;
        baseDomain = "${cfg.headscale.zone}";
      };
      settings = {
        logtail.enabled = false;
        ip_prefixes = [ "100.64.0.0/10" ];
      };
    };

    services.nginx.virtualHosts.${cfg.headscale.domain} = {
      forceSSL = true;
      enableACME = true;
      locations."/" = {
        proxyPass = with config.services.headscale;
          "http://${address}:${toString port}";
        proxyWebsockets = true;
      };
    };

    environment.systemPackages = [ config.services.headscale.package ];

    dns.zones.${cfg.headscale.zone}.records = [
      {
        name = "${cfg.headscale.domain}.";
        type = "CNAME";
        data = "vps";
      }
    ];
  };
}

(See github.com/RyanGibb/eilean-nix/blob/7383eb/modules/headscale.nix)

To initialize a namespace, on the server we run:

headscale namespaces create <namespace_name>

Then on our remarkable we can run:

$ sudo /opt/bin/tailscale up --login-server headscale.freumh.org --hostname remarkable

Which will give us a URL to a webpage that gives a command to register the device, which will look something like:

headscale --namespace <namespace_name> nodes register --key <machine_key>

And now we're in!


  1. See github.com/RyanGibb/eilean-nix/tree/0b4213/modules/dns/default.nix↩︎