November 19, 2017

Fedora 26: Terminus Font renamed

Quick note: In Fedora 26, the Terminus font was renamed. It is now called “xos4 Terminus”. If you use rxvt-unicode, you will have to adjust your .Xresources. For my setup, the relevant line now looks like this:

URxvt.font: xft:xos4 terminus:pixelsize=16

October 30, 2017

Implementing IP Multicast over Ethernet with LwIP and ChibiOS on a STM32F767ZI

For my master’s thesis, I work with STM32F767 Nulceo-144 boards. We are using ChibiOS as operating system and needed support for IP Multicast. This blog post describes what is necessary to add Multicast support to this setup. Overall, it was a lot of fun and I learned a lot!

First of all, let’s take a quick look at how Multicast works. Multicast means that we address a single packet to several peers. This works by using special addresses. For IPv4 these are 224.0.0.0 to 239.255.255.255 (224.0.0.0/4). If a host wants to receive packets for a Multicast address, it must join the respective Multicast Group. It does so using IGMP. Then, the router in our network knows that the host wants to receive traffic for the respective Multicast Group and will start forwarding traffic.

IP Multicast over Ethernet

In an Ethernet Collision Domain, multiple hosts may want to receive Multicast traffic. If we would just send traffic to the host’s regular MAC address, the router would have to duplicate packets, if there is more than one host receiving multicast traffic. To avoid this, there are special Multicast MAC addresses. Similarily to a Broadcast MAC address, they address multiple hosts. These MAC addresses are 01-00-5E-00-00-00 to 01-00-5E-7F-FF-FF. As we have 23 Bits for in the Multicast MAC range, but 28 Bits in the Multicast IP range, this is not a one-to-one mapping. Instead, multiple Multicast IPs map to a single Multicast MAC address. The mapping works by OR-ing the lower 23 Bits of the Multicast IP with the Multicast base MAC address (01-00-5E-00-00-00).

When the Ethernet MAC of our microcontroller (µC) receives a Ethernet frame, it takes a look at the destination MAC address of the frame. If the frame is not addressed to the µC, i.e. its destination MAC address does not match the MAC address of the µC, it is simply discarded by the MAC. This saves resources, as the microcontroller can continue working on other tasks and no CPU time must be spent handling packets that are not meant for us.

For Multicast, this is of course not what we want: the frames are not sent to our MAC address, but to the Multicast MAC address! Thus, the MAC in our µC will simply discard the frame and it will never be seen by software. This means we have to tell hardware that it should accept frames for the MAC address of the Multicast group we want to join.

LwIP and Multicast

Fortunately, LwIP already comes with Multicast support. You can join a multicast group using the netconn_join_leave_group() function. It will take care of IGMP for you. But, with the stock ChibiOS MAC drivers, it will not take care of configuring your MAC to accept packets for the Multicast MAC address! So, the MAC driver has to be notified when we join a Multicast Group. For this, LwIP allows you to register a callback using the netif_set_igmp_mac_filter() function. This callback can then take care of configuring the MAC appropriately.

Configuring the MAC to accept frames for the Multicast MAC address

Now to the fun low-level part! Let’s take a look at the STM32F76xxx Reference Manual, section 42.5.5 “MAC filtering”. We have several options here:

  1. Set the Pass All Multicast (PAM) bit in the MACFFR register. Then, the MAC will pass all Multicast frames to software. This is of course very easy to implement. We do not even have to use LwIP’s igmp_mac_filter. We can just set the respective bits when the MAC is initialized. This is, however, a quick and dirty solution, as all Multicast frames then have to go through the LwIP stack to decide whether we are interested in them. This means we will waste quite some CPU cycles just for this. Thus, I did not choose this solution.
  2. Use perfect filtering and write the Multicast address to one of the four MAC address registers. However, this limits the number of Multicast groups we are able to join. As the device itself needs a MAC address, we can join at most three Multicast groups.
  3. Use imperfect filtering using the Hash Table. This is a good compromise between aoproach 1 and 2: We can join as many Multicast Groups as we want, but we will only waste CPU cycles if there has been a collision in the hash table. I chose this approach, even though it is probably the most difficult to implement.

The MAC hash table

Let’s take a look at how the MAC hash table works. When the MAC receives a frame, it extracts the destination address. It will then compute the CRC-32 of this MAC address, which results in a 32-bit value. Only the upper six bits are then used for the hash table.

The hash table consists of two registers: the Ethernet MAC hash table low register (ETH_MACHTLR) and the Ethernet MAC hash table high register (ETH_MACTHR). Each of these registers is 32 Bits, which means that the hash table is 64 Bits in total.

The most significant bit of the calculated CRC value indicates whether the ETH_MACTLR or the ETH_MACTHR is consulted. The next five bits of the CRC then indicate the index of the bit in the register that is checked. If the respective bit is one, the frame passes the MAC filter, otherwise it does not.

So, in our igmp_mac_filter callback, we have to do the following steps:

  1. From LwIP, we receive an IP address as a parameter to the callback. We first have to convert this to a MAC address, as explained above.
  2. We then have to calculate the CRC-32 of this MAC address.
  3. Finally we set the appropriate bits in the ETH_MACTHR or ETH_MACTLR registers.

Keep in mind that Ethernet transmits the least significant bit first. Weirdly, this also applies to the CRC calculation above. The most significant bit of the CRC is actually the least significant bit and so on.

I will publish the code as soon as I’ve tested my implementation thoroughly and all the bugs are fixed. Second blog post with a deep dive into the code will follow!

May 6, 2017

Request Let’s Encrypt Certificates with Ansible

Since version 2.2, Ansible comes with the letsencrypt module. This allows you to request certificates from letsencrypt. Unfortunately, I’ve found the documentation to be a bit lacking, so I want to document what I’ve done to get this working. If your distribution doesn’t have Ansible 2.2 yet, you can very easily install it using pip:

pip install --user ansible

As always, this is not a copy & paste tutorial, but a collection of ideas that you have to adjust to your environment.

First of all, you need an account key. The Ansible documentation doesn’t really tell you how you have to create that, so here is an Ansible task that does just that:

- name: Create Lets Encrypt Account Key
  command: "openssl genrsa -out {{ letsencrypt_account_key }}"
  args:
      creates: "{{ letsencrypt_account_key }}"

You also may want to make sure that the key is protected from unauthorized access:

- file:
    path: "{{ letsencrypt_account_key }}"
    owner: root
    group: root
    mode: 0600

Now we can start with the certificates. For this, it is important to know that the host to which the playbook is applied handles the communication with Let’s Encrypt. This means that it is not sufficent if the host on which you run Ansible can make outbound connections to the Let’s Encrypt servers. You have to adjust your firewall to allow outbound connections from the host the playbook is being applied to to the Let’s Encrypt servers. I usually run a small proxy server (for example, tinyproxy for such outgoing connections. Ansible will respect the http_proxy and https_proxy environment variables.

sslvhost.yml will handle key generation and requesting the certificates from Let’s Encrypt. We call this tasklist for each certificate we want to request. This is also a great place for the proxy environment variables, if you use one:

- name: Generate Certificates for SSL
  include: sslvhost.yml
  vars:
    domain: "example.com"
    key_file: "{{ letsencrypt_key_dir }}/example.com-key.pem"
    req_file: "{{ letsencrypt_cert_dir }}/example.com-req.pem"
    cert_file: "{{ letsencrypt_cert_dir }}/example.com-cert.pem"
  environment:
      http_proxy: http://httpproxy:8080/
      https_proxy: http://httpproxy:8080/

In sslvhost.yml we first create a private key and make sure it has appropriate permissions:

- name: "Generate HTTPS Private Key"
  command: "openssl genrsa -out {{ key_file }} {{ rsa_key_size }}"
  args:
      creates: "{{ key_file }}"

- file:
    path: "{{ key_file }}"
    mode: 0600
    owner: root
    group: root

Now, we need a Certificate Signing Request (CSR). By default, OpenSSL prompts the user for some values when doing so. For Let’s Encrypt, everything except the Common Name is unimportant. Thus we can just supply some dummy values:

- name: Generate CSR
  command: "openssl req -new 
      -key {{ key_file }} 
      -out {{ req_file }}
      -nodes -subj '/C=US/ST=SomeState/L=City/O=Org/CN={{ domain }}'"
  args:
      creates: "{{ req_file }}"

- file:
    path: "{{ req_file }}"
    mode: 0600
    owner: root
    group: root

Now we can request the challenge from Let’s Encrypt, deploy the challenge and retrieve the certificate:

- name: Requesting Lets Encrypt Challenge
  letsencrypt:
    account_email: "{{ letsencrypt_account_email }}"
    account_key: "{{ letsencrypt_account_key }}"
    csr: "{{ req_file }}"
    dest: "{{ cert_file }}"
  register: challenge

- copy:
    dest: "{{ challenge_file_name }}"
    content: "{{ challenge_file_content }}"
    mode: 0644
  when: challenge|changed 
  vars:
      challenge_file_name: "{{ letsencrypt_challenge_dir }}/{{ challenge['challenge_data'][domain]['http-01']['resource'] }}"
      challenge_file_content: "{{ challenge['challenge_data'][domain]['http-01']['resource_value'] }}"

- name: Responding To Challenge and Fetching Certificate
  letsencrypt:
    account_email: "{{ letsencrypt_account_email }}"
    account_key: "{{ letsencrypt_account_key }}"
    csr: "{{ req_file }}"
    dest: "{{ cert_file }}"
    data: "{{ challenge }}"

Of course, this assumes that letsencrypt_challenge_dir is being served by your webserver. Note that challenges are deployed to {{ letsencrypt_challenge_dir }}/.well-known/acme-challenge. For nginx, the following line should do the job:

location /.well-known/acme-challenge/ {
    alias {{ letsencrypt_challenge_dir }}/.well-known/acme-challenge/;
}

May 1, 2017

Wallabag and zeit.de Paywall

Wallabag supports paywalls. To make the zeit.de paywall work, just add the following snippet to /vendor/j0k3r/graby-site-config/zeit.de.txt:

requires_login: true
not_logged_in_xpath: //aside[@class='gate']
login_uri: https://meine.zeit.de/anmelden
login_username_field: email
login_password_field: pass

Afterwards, add your zeit.de credentials to /app/config/parameters.yaml:

sites_credentials: 
    zeit.de: { username: "you@example.com", password: "hunter" }

Important: This is a YAML file, which only allows spaces, but no tabs, for whatever reason.

Then, under “Internal Settings” > “Article” set “Enable authentication for websites with paywall” to “1”.

After you’ve cleaned your cache, you can now read articles of zeit.de behind a paywall in your Wallabag!

April 18, 2017

FreshRSS: Better Integration with Wallabag

I’ve started using FreshRSS for my RSS feeds a few days ago. It’s a great software that works pretty well for me. But there is one thing I’ve found a little annoying: I like to go through the headlines of my RSS feeds on my phone and then save the articles I want to read to Wallabag. FreshRSS already supports this through its share function, but it is a little inconvenient. Every time I add an article to Wallabag, it opens a new tab which I have to close. Especially on my mobile, this is annoying.

To solve this problem, I’ve created a small hackish extension for FreshRSS that solves this problem. Instead of opening a link on the client side, it adds the article to Wallabag from the server side using the Wallabag v2 REST API. Thus, no new tab is opened and you are not interrupted while reading through your headlines.

Check out the plugin at https://git.n7r.de/nrb/freshrss-wallabag.

Powered by Hugo & Kiss.