please enable javascript

Setting up ipad screen mirroring on nixos

Alexander Comerford
December 27th, 2022 · 2 min read

Recently I bought an iPad so I can live share my notes to my laptop during screencasts. This is incredibly useful for online meetings, presentations, or pair programming sessions where I want to show notes to whoever in real-time.

In this blog post, I'll explain how I setup screen mirroring from my iPad to a NixOS based setup. Whether you are using NixOS as your daily driver or just want to try out screen mirroring on a *nix based system, hopefully my experience and learnings can be helpful for you.

Prerequisites

  1. An iPad with screen mirroring capabilities.
  2. A laptop or desktop computer running NixOS.
  3. Both devices connected to the same local network

Note that for the rest of this tutorial I'm running NixOS 22.11 (Raccoon) and an iPad (6th gen) running iOS 16.2.

Setting up an AirPlay receiver on NixOS

Apple devices canonically talk to each other via AirPlay, so in order to mirror an iPad to NixOS, we need to setup an AirPlay "receiver" server.

There are many open source AirPlay receivers, but I chose to use UxPlay simply because it was the first one I came across and it's in nixpkgs.

Now unfortunately if we start running the receiver via nix run nixpkgs#uxplay, the iPad won't see anything 😓. This is expected though because the receiver has no way of interacting with the service discover mechanism of AirPlay which is mDNS.

The UxPlay README recommends using Avahi for doing mDNS. Luckily Avahi is available for NixOS so we can add it too our configuration.nix as follows:

1services.avahi = {
2 nssmdns = true;
3 enable = true;
4 publish = {
5 enable = true;
6 userServices = true;
7 domain = true;
8 };
9};

Now after a nixos-rebuild switch we should be running Avahi

1$ systemctl status avahi-daemon.service
2* avahi-daemon.service - Avahi mDNS/DNS-SD Stack
3 Loaded: loaded (/etc/systemd/system/avahi-daemon.service; enabled; preset: enabled)
4 Active: active (running) since Tue 2022-12-27 09:12:57 EST; 13s ago
5TriggeredBy: * avahi-daemon.socket
6 Main PID: 245105 (avahi-daemon)
7 Status: "Server startup complete."
8 IP: 8.8K in, 8.8K out
9 IO: 0B read, 0B written
10 Tasks: 1 (limit: 38114)
11 Memory: 628.0K
12 CPU: 22ms
13 CGroup: /system.slice/avahi-daemon.service
14 `-245105 "avahi-daemon: running [spam.local]"
15
16Dec 27 09:12:57 spam avahi-daemon[245105]: New relevant interface lo.IPv4 for mDNS.
17Dec 27 09:12:57 spam avahi-daemon[245105]: Network interface enumeration completed.
18...

Now if we try running UxPlay again, we should see our AirPlay receiver show up in the screen mirror.

imgmy iPad seeing UxPlay!

This is great! However if we try to connect we will just end up with a timeout. What gives?

After breaking out Wireshark it was obvious that is was a firewall issue. The simple remedy to this problem was to set networking.firewall.enable = false; in the configuration.nix. But this is in general a bad idea and should be avoided.

Instead I went with a uxplay + iptables + bash approach so while the uxplay is running, iptables will allow incoming traffic to all necessary ports, and on exit, will close them again.

1#! /usr/bin/env nix-shell
2#! nix-shell --quiet -p uxplay -i bash
3
4set -ueo pipefail
5
6## Clear any existing "DROP ME" rules
7## ref: https://stackoverflow.com/a/63855690/12393422
8while sudo iptables -L -n --line-number | grep "DROP ME" > /dev/null; do
9 sudo iptables -D INPUT $(sudo iptables -L -n --line-number | grep "DROP ME" | head -1 | awk '{print $1}');
10done
11
12
13LOCAL_CIDR=${1:-"192.168.0.0/16"}
14
15open-port-tcp() {
16 local port=$1
17 echo "Opening tcp port $port from $LOCAL_CIDR ..."
18 sudo iptables \
19 -I INPUT \
20 -p tcp \
21 -s $LOCAL_CIDR \
22 --dport $port \
23 -j ACCEPT \
24 -m comment --comment "DROP ME"
25}
26
27close-port-tcp() {
28 local port=${1:-0}
29 echo "Closing tcp port $port from $LOCAL_CIDR ..."
30 sudo iptables \
31 -D INPUT \
32 -p tcp \
33 -s $LOCAL_CIDR \
34 --dport $port \
35 -j ACCEPT \
36 -m comment --comment "DROP ME"
37}
38
39open-port-udp() {
40 local port=$1
41 echo "Opening udp port $port from $LOCAL_CIDR ..."
42 sudo iptables \
43 -I INPUT \
44 -p udp \
45 -s $LOCAL_CIDR \
46 --dport $port \
47 -j ACCEPT \
48 -m comment --comment "DROP ME"
49}
50
51close-port-udp() {
52 local port=${1:-0}
53 echo "Closing udp port $port from $LOCAL_CIDR ..."
54 sudo iptables \
55 -D INPUT \
56 -p udp \
57 -s $LOCAL_CIDR \
58 --dport $port \
59 -j ACCEPT \
60 -m comment --comment "DROP ME"
61}
62
63open-port-tcp 7100
64open-port-tcp 7000
65open-port-tcp 7001
66open-port-udp 6000
67open-port-udp 6001
68open-port-udp 7011
69
70# Ensure port closes if error occurs.
71trap "close-port-tcp 7100 && \
72 close-port-tcp 7000 && \
73 close-port-tcp 7001 && \
74 close-port-udp 6000 && \
75 close-port-udp 6001 && \
76 close-port-udp 7011
77 " EXIT
78
79uxplay -p
1$ ipad_screen_mirror_server
2Opening tcp port 7100 from 192.168.0.0/16 ...
3Opening tcp port 7000 from 192.168.0.0/16 ...
4Opening tcp port 7001 from 192.168.0.0/16 ...
5Opening udp port 6000 from 192.168.0.0/16 ...
6Opening udp port 6001 from 192.168.0.0/16 ...
7Opening udp port 7011 from 192.168.0.0/16 ...
8using network ports UDP 7011 6001 6000 TCP 7100 7000 7001
9using system MAC address xx:xx:xx:xx:xx:xx
10Initialized server socket(s)

Here is the gist for those interested.

Now wrapping all this code into a simple script gives us an easy way to easily spin up an AirPlay receiver server!

If you want to learn more about screen mirroring or NixOS, here are some additional resources you might find helpful:

I hope you found this useful, let me know if you have any other tips or tricks for screen mirroring on NixOS!

(ง ͠° ͟ل͜ ͡°)ง

More posts from The Art of Abstraction

Hosting a browser shell with v86 and buildroot

Click -> OS

October 31st, 2022 · 1 min read

Writing a Weaver solver

Beating word games one letter at a time

October 4th, 2022 · 2 min read