How I got Roon working over OpenVPN (hard for me, easy for you)

So I explained in another thread how to get Roon working across VLAN’s, but it didn’t work over a VPN like OpenVPN. OpenVPN is super easy to setup and works on almost all devices, but as many of you know, it’s not easy to get it to work with Roon.

So I wrote some code which does the necessary gymnastics to forward packets from tun interfaces like OpenVPN uses and standard Ethernet interfaces your home router is going to use. Depending on various variables, it may or may not work directly with WiFi interfaces (your home router has built in wifi). Yes, this works with OpenVPN in Layer 3 mode. The whole point was to make this easy. :slight_smile:

How do you use it? Well, grab a binary from the releases page: https://github.com/synfinatic/udp-proxy-2020/releases/tag/v0.0.4 or you can build your own from the source code provided. Yes, there are Linux/MIPS64 binaries for all you Ubiquiti USG users and FreeBSD binaries for pfSense 2.4 too. The static Linux binary should work on most modern Linux/Intel or AMD servers. The Darwin binary is for MacOS. If someone needs a Linux/ARM binary let me know and I’ll see what I can do to setup the necessary cross-compile chain.

And then: ./udp-proxy-2020 --port 9003 --interface eth0,eth1,tun0

and replace the list of interface names at the end with the names of the interfaces on your firewall.

5 Likes

Another brilliant action from @Aaron_Turner!
Roon should have made this possible from the start, but the Roon community needs members like this to make it happen!

Thanks @ogs. I should mention, just in case it wasn’t obvious that udp-proxy-2020 also supports Roon devices on multiple local subnets/VLAN’s- not just OpenVPN tunnels.

It should also work on pretty much any VPN tunnel assuming your router/firewall pre-configures the network interface for inbound VPN tunnels. This prevents it from working with certain IPSec configurations which don’t use virtual tunnel interfaces, but I figure most people would rather use OpenVPN than IPSec+MOBIKE so probably not the end of the world.

Excellent work, however all of this has just gone way over my head so not sure where the easy part comes into it, but well done, whatever you did!

@Mark_Hyland: If you need Roon to work across multiple subnets/VLAN’s or a VPN feel free to ask questions. I’m happy to help.

The nice thing if VLANs and VPN’s sound like nonsense networking acronyms then you probably don’t need udp-proxy-2020 in which case, I’d probably say you’re luckly and you should spend more time listening to music and less time reading the forums. :slight_smile:

The network software in the Roon system is rather not optimal as well as in several other areas - as I wrote before.
But subscribers love is blind… :wink:

I guess around 90% of support is network related. Most of that would not exist if discovery protocols etc. was different or had fallback to using more conventional networking. They probably regret this choice, but it is maybe a bit late in the game now…
This would probably have made remote networking easier to do too.

So I get how and why Roon did what they did. Someone wearing a Product Management hat early on said something like “Roon devices need to find the Roon core without any configuration because people may not know how to configure a static IP for their core and it might change and we’d get tons of complaints.”

Then, some software engineer who didn’t understand how networking works (this is like 99% of software developers by the way) did this UDP broadcast thing… probably because they read something on stack overflow.

At first, everything was fine. But then Roon got more popular and more advanced network configs & users started using Roon and wanting it to work across subnets or over a VPN while they were at work or on vacation. But those were a small minority and when it came down to deciding on working on features like Valance vs. “fix networking”, Valance/etc always won because it was more sexy, would help the company grow and impact more users.

It probably didn’t help that when Roon looked at this, they saw that the use cases, hardware and software configs were all over the map. Even if they could come up with a better way, how could they test and support all these configs?

Anyways, writing that program for me is fun and gave me an excuse to write more Go code which is a language I’m learning.

You may be right on much of what you write, but the quote below is a bit harsh. It was most likely Brian Luczkiewicz who wrote this code. I know Brian as a brilliant programmer so I don’t think this is a good match. Roon networking is quite robust actually. The discovery part is more of a design decision I think.

I don’t know Brian. And my previous message shouldn’t be taken as a slight against him or anyone.

I’ve known many really intelligent and capable software engineers who don’t grok deeper networking concepts. Reality is that there are many areas of software development and it’s impossible for even the best developers in the world to understand all the ins and outs of everything. I get networking, security, backend systems, DevOps and can do simple embedded systems, but I can’t do a GUI, web development (well I can sorta fake this but you’re not going to like the results), graphics or ML (among others).

That said, my problem with discovery isn’t so much as the design (I totally understand how they got here), but rather in the implementation. Simply put, if you want to send a UDP broadcast message like is used on UDP/9003 you’d do something like this:

bcast_sock = socket(AF_INET, SOCK_DGRAM, 0);
int broadcastEnable=1;
int ret=setsockopt(bcast_sock, SOL_SOCKET, SO_BROADCAST, &broadcastEnable, sizeof(broadcastEnable));
if ret > 0 {
   perror();
   exit(errno);
}

For the record, I looked that up on stackoverflow because it’s been about a decade since I’ve written any C. :slight_smile: The key thing here is if you want to send a broadcast message over UDP you have to explicitly tell the computer you want to do so and it will return an error if you make a mistake.

Now let’s look at a two different network interfaces on my computer:

$ ifconfig en0
en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
       [SNIP]

$ ifconfig utun0
utun0: flags=8051<UP,POINTOPOINT,RUNNING,MULTICAST> mtu 1380
	[SNIP]

If you look at the first line of output from each ifconfig command you see a set of FLAGS IN CAPITAL LETTERS. Things like “UP” and “RUNNING” and “BROADCAST”. Oh wait, BROADCAST only shows up on en0 (my wifi interface) and not tunnel interface utun0. Instead we see POINTTOPOINT because this is not a network interface which supports broadcasts at all. (On a side note, notice both interfaces support Multicast messages!)

So the code sets the BROADCAST flag on the socket and we have a network interface which doesn’t support BROADCAST messages. What does the code do? Well my code above exits with an error because you can’t do that.

Roon does something different. I’m actually kinda curious what exactly… I have this feeling they hard coded the last octet to .255 in the IP address assuming everyone just uses a /24. I know at one point in time when I was testing my code I was using a /30 for my POINTTOPOINT VPN tunnel and was still seeing “broadcast” messages like they were being sent on a /24. I’d have to do more testing to understand what is up with that. It’s this rather non-standard/incompatible behavior that prevents udp-broadcast-relay-redux from working on OpenVPN tunnel interfaces like this and is why I wrote udp-relay-2020.

But Roon doesn’t try sending an Ethernet header on these “loopback” interfaces which is good. So Roon is doing something right.

The irony is that because Roon does this UDP broadcast incorrectly, it still sends a packet. This is really really good news! Because if it correctly determined that a OpenVPN tunnel interface did not support broadcast messages and just skipped that interface, then it would be MUCH MUCH more difficult to get this to work over a VPN tunnel (would require code to run on the remote device with the OpenVPN client).

Honestly, this totally could just be some weird .NET bug when it runs on a non-Windows platform and it’s Microsoft’s fault. I wouldn’t be shocked. Like I said, networking is one of those areas of programming that 99% of developers don’t really understand that well.

1 Like

Ah ah! Thanks!
I need to try this. I have a Raspberry Pi with Wireguard perfectly running on it, but not with Roon yet. I will try your suggestions, I might even switch to openvpn for Roon…

Thanks,
Francesco

@Francesco: I haven’t used Wireguard, but according to the docs it creates an interface like wg0. It might just work… if not, please provide the output of:

ifconfig wg0

and I’ll see what I can do.

Let’s just flag @brian. I should have done that in my previous post, but I forgot… your input is certainly very interesting and advanced.

@Aaron_Turner
I’ve compiled the application from sources after installing go on my RPi. Compilation looked ok and it created an executable under dist directory.
However, when I run it I get an error:

root@rpi4:~/BUILD/udp-proxy-2020-0.0.4# dist/udp-proxy-2020-0.0.4-linux-armv7l --port 9003 --interface eth0,wg0
FATA[0001] wg0: has an invalid layer type: 0x526177

ifconfig reports the following (I’m skipping the loopback interface here…):

root@rpi4:~/BUILD/udp-proxy-2020-0.0.4# ifconfig -a
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.0.172  netmask 255.255.255.0  broadcast 192.168.0.255
        inet6 fe80::dea6:32ff:fe0e:3691  prefixlen 64  scopeid 0x20<link>
        ether dc:a6:32:0e:36:91  txqueuelen 1000  (Ethernet)
        RX packets 266149069  bytes 4168895575 (3.8 GiB)
        RX errors 10412  dropped 12649  overruns 0  frame 1
        TX packets 244320698  bytes 870887899 (830.5 MiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

wg0: flags=209<UP,POINTOPOINT,RUNNING,NOARP>  mtu 1420
        inet 10.6.0.1  netmask 255.255.255.0  destination 10.6.0.1
        unspec 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00  txqueuelen 1000  (UNSPEC)
        RX packets 84816667  bytes 37789103564 (35.1 GiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 122753772  bytes 123997138256 (115.4 GiB)
        TX errors 290628  dropped 1421597 overruns 0  carrier 0  collisions 0

Any idea?
Thanks in advance,
Francesco

Edit:
Some more verbose output with --debug:

root@rpi4:~/BUILD/udp-proxy-2020-0.0.4# dist/udp-proxy-2020-0.0.4-linux-armv7l --port 9003 --interface eth0,wg0 --debug
DEBU[0000]/root/BUILD/udp-proxy-2020-0.0.4/cmd/listen.go:40 main.newListener() eth0: ifIndex: 2                             
DEBU[0000]/root/BUILD/udp-proxy-2020-0.0.4/cmd/listen.go:50 main.newListener() eth0 network: ip+net		string: 192.168.0.172/24 
DEBU[0000]/root/BUILD/udp-proxy-2020-0.0.4/cmd/listen.go:50 main.newListener() eth0 network: ip+net		string: fe80::dea6:32ff:fe0e:3691/64 
DEBU[0000]/root/BUILD/udp-proxy-2020-0.0.4/cmd/listen.go:82 main.newListener() Listen: {eth0 0x1c768e0 [9003] 192.168.0.255 false <nil> 250000000 0 0x1c68640 map[]} 
DEBU[0000]/root/BUILD/udp-proxy-2020-0.0.4/cmd/listen.go:40 main.newListener() wg0: ifIndex: 4                              
DEBU[0000]/root/BUILD/udp-proxy-2020-0.0.4/cmd/listen.go:82 main.newListener() Listen: {wg0 0x1c76d60 [9003]  true <nil> 250000000 0 0x1c68a80 map[]} 
DEBU[0000]/root/BUILD/udp-proxy-2020-0.0.4/cmd/interfaces.go:54 main.initializeInterface() eth0: applying BPF Filter: udp port 9003     
DEBU[0000]/root/BUILD/udp-proxy-2020-0.0.4/cmd/interfaces.go:66 main.initializeInterface() Opened pcap handle on eth0                   
FATA[0000]/root/BUILD/udp-proxy-2020-0.0.4/cmd/interfaces.go:49 main.initializeInterface() wg0: has an invalid layer type: 0x526177

Just tried a few more things, as far as I can.
I’m not a network expert and I do not know Go, so everything I have tried could be non-sense…

The problem that is reported above comes from the fact that the link type for the interface wg0 is “12”, which does not corrspond to any of the values in the LinkType enum. I’ve seen that in your code you accept three different types:

// List of LayerTypes we support in sendPacket()
var validLinkTypes = []layers.LinkType{
	layers.LinkTypeLoop,
	layers.LinkTypeEthernet,
	layers.LinkTypeNull,
}

If I add “12” to the list above, it passes the init step, but it obviously fails in “sendPacket” or “learnClientIP” as LinkType 12 is not handled.
I’ve tried to add the case in the switch, trying some combination of “NewDecodingLayerParser”, but nothing that works. Best case, when I try Roon from VPN, I see the following in the console where udp-proxy-2020 is running:

DEBU[0007]/root/BUILD/udp-proxy-2020-0.0.4/cmd/send.go:27 main.(*SendPktFeed).Send() Lock()???                                    
DEBU[0007]/root/BUILD/udp-proxy-2020-0.0.4/cmd/send.go:29 main.(*SendPktFeed).Send() Lock() achieved.  Sending out 1 interfaces   
DEBU[0007]/root/BUILD/udp-proxy-2020-0.0.4/cmd/send.go:34 main.(*SendPktFeed).Send() wg0: sending out because we're not eth0      
DEBU[0007]/root/BUILD/udp-proxy-2020-0.0.4/cmd/send.go:36 main.(*SendPktFeed).Send() wg0: sent                                    
DEBU[0007]/root/BUILD/udp-proxy-2020-0.0.4/cmd/listen.go:143 main.(*Listen).sendPackets() processing packet from wg0 on eth0           
WARN[0007]/root/BUILD/udp-proxy-2020-0.0.4/cmd/listen.go:161 main.(*Listen).sendPackets() Unable to decode packet from wg0: Invalid loopback protocol "E\x00\x01m" 
DEBU[0007]/root/BUILD/udp-proxy-2020-0.0.4/cmd/send.go:39 main.(*SendPktFeed).Send() Unlock()                                     

Or:

DEBU[0007]/root/BUILD/udp-proxy-2020-0.0.4/cmd/listen.go:119 main.(*Listen).handlePackets() wg0: received packet and fowarding onto other interfaces 
DEBU[0007]/root/BUILD/udp-proxy-2020-0.0.4/cmd/send.go:27 main.(*SendPktFeed).Send() Lock()???                                    
DEBU[0007]/root/BUILD/udp-proxy-2020-0.0.4/cmd/send.go:29 main.(*SendPktFeed).Send() Lock() achieved.  Sending out 1 interfaces   
DEBU[0007]/root/BUILD/udp-proxy-2020-0.0.4/cmd/send.go:34 main.(*SendPktFeed).Send() wg0: sending out because we're not eth0      
DEBU[0007]/root/BUILD/udp-proxy-2020-0.0.4/cmd/send.go:36 main.(*SendPktFeed).Send() wg0: sent                                    
DEBU[0007]/root/BUILD/udp-proxy-2020-0.0.4/cmd/listen.go:143 main.(*Listen).sendPackets() processing packet from wg0 on eth0           
WARN[0007]/root/BUILD/udp-proxy-2020-0.0.4/cmd/listen.go:177 main.(*Listen).sendPackets() Packet from wg0 did not contain a IPv4/UDP packet 

And I’m completely stuck here…

Hi @Francesco,

I can’t say I’m totally surprised. I’ll have to look into the actual link type wg interfaces use and add support. POINTTOPOINT interfaces generally also have some kind of header which will need to be crafted & decoded when sending/receiving.

Should have more info for you soon-ish.

Thanks a lot, really appreciated.
If you need any information, run some debug test in my system or whatever, please let me know (PM is probably a better way…)

Looks like Wireguard uses the RAW LinkType which is actually pretty standard and was easy to add support for. I don’t have cross compiling for Linux/ARM64 for the RasPi setup yet, so non-trival to provide you a binary… but since you already compiled it once, hopefully this won’t be too difficult. :slight_smile:

I haven’t actually tested it for RAW, but I haven’t broken the code for FreeBSD/OpenVPN so this change wasn’t a total dumpster fire!

Anyways, if you grab the latest code from github and recompile and let me know how it works that would be appreciated. I’ve merged the change to the main branch so you can just do a:

git clone https://github.com/synfinatic/udp-proxy-2020.git

and compile from there.

Incase you’re curious about the changes: https://github.com/synfinatic/udp-proxy-2020/pull/30/files

FYI: that change didn’t work. if anyone can get me a pcap of the network traffic running over a wg interface that would help a lot:

tcpdump -i wg0 -s0 -w wireguard.pcap -c 10

Then ping the remote host or something over the VPN. Just need a few (in this case 10) packets. Then please attach the pcap file to the ticket: https://github.com/synfinatic/udp-proxy-2020/issues/29

Different time zone most likely, I could do it only this morning (Europe…)
Done!

1 Like