Setting up ipad screen mirroring on nixos

5 min read Original article ↗

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

3

4set -ueo pipefail

5

6

7

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

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!

(ง ͠° ͟ل͜ ͡°)ง