Introduction: About Communication Between Garmin SDK and a Raspberry Pi

Garmin Connect IQ provides a nice SDK to build Widges, Apps and Data Fields for Garmin watches.
A Garmin Connect IQ App on the phone (android, iOS) works as the companion app connected by BLE. That companion app synchronizes data (calendar, weather, etc.), handles notifications and also implements the Wi-Fi communication of apps running on the watch. These communications are piped through the phone by BLE and from there by Wi-Fi to the network. The SDK is straightforward and easy to use, so I wrote a Watch Widget to control my Smart Home (Home Assistant) and entertainment center(Kodi) as well as communication with my little self-made server.

Step 1: The HTTPS Restriction

Android introduced a network-security-configuration that restricts clear-text-traffic (HTTP) to just a few domains after updating Android to version 10.
This restriction allows exceptions for clear-text-traffic defined in a network_security_config.xml, but unfortunately that list does not support a white-list for a specific range of IP addresses like 192.168.0.0/16. And as Garmin uses that restriction, every Watch app inherits this restriction as well. Here is Garmin's network_security_config.xml that shows the whitelist of domains.

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <debug-overrides>
        <trust-anchors>
            <certificates src="user" />
        </trust-anchors>
    </debug-overrides>
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="true">garmin.com</domain>
        <domain includeSubdomains="true">garmin.cn</domain>
        <domain includeSubdomains="true">garmincdn.com</domain>
        <domain includeSubdomains="true">180.168.57.218</domain>
        <domain includeSubdomains="true">180.168.57.219</domain>
        <domain includeSubdomains="true">strava.com</domain>
        <domain includeSubdomains="true">127.0.0.1</domain>
        <domain includeSubdomains="true">localhost</domain>
    </domain-config>
</network-security-config>

As a result any communication to a device in the local network (like a Raspi) ends with the error code 300.
In the following I evaluate different approaches to still allow communication to a Raspberry Pi.

Step 2: Localhost and Port Forwarding

As you can see one HTTP-exception in the network_security_config.xml is the localhost or 127.0.0.1.
This allows watch apps to communicate to a server running on the phone. Fwd: port forwarder https://play.google.com/store/apps/details?id=com.elixsr.portforwarder&gl=DE is an android app that implements a server acting as port forwarder. Here you can define what port should link to what URL and port. Since the Connect IQ App still talks to the localhost server, the HTTP-restriction to localhost is fulfilled.

Unfortunately the forwarder server on Android is not reliable. When the phone is locked for a while the server stops working. Which makes sense for power optimization reasons. But in the end, a port forwarder server is not practical here.

A correct solution would rather be a direct port forwarding with iptables. That would require to root/jailbreak the phone and lose warranty. So i decided to search another way.

Step 3: Modifing the Garmin-connect.apk

Another idea is to fix the bug in the Garmin Connect IQ App.
I downloaded garmin-connect_4.44.apk on chrome by the Chrome extension APK DOWNLOADER. I also installed the Android SDK to get adb and other tools. And I used the apktool 2.5.0 from https://ibotpeaches.github.io/Apktool/install/

Once you have the apk and jar decompile the apk.

java -jar apktool.jar d -f garmin-connect_4.44.apk -o decompile/

Override the security_config in decompile/res/xml/network_security_config.xml by allowing clear-text-traffic.

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="true" />
</network-security-config>

Now compile the fixed app again.

java -jar apktool.jar b --use-aapt2 -f -d decompile

cp decompile/dist/garmin-connect_4.44.apk garmin-connect_4.44_unsigned.apk

Obviously that breaks the signing of the recompiled app in the dist-folder. To sign the app again, you first need a key.

keytool -genkey -v -keystore my-garmin-key.keystore -alias garmin-key -keyalg RSA -keysize 2048 -validity 10000

Now you can sign the app with your new key.

jarsigner -sigalg SHA1withRSA -digestalg SHA1 -keystore my-garmin-key.keystore garmin-connect_4.44_unsigned.apk garmin-key
zipalign.exe -p 4 garmin-connect_4.44_unsigned.apk garmin-connect_4.44_signed.apk
apksigner.bat verify garmin-connect_4.44_signed.apk

Finally, install the app on your phone.

adb.exe install garmin-connect_4.44_signed.apk

After the installation the app starts as expected. Entering invalid user and password is answered by an expected error message. However, entering correct credentials result in the toast-notification "java.lang.Exception: 401" That looks like a protection of Garmin's backend that limits communication to correctly signed apps. Which would be a dead end for that approach.

Step 4: SSL Certificate for the Router

But what about securing the Raspi by HTTPS communication? How would that work?
HTTPS means a former Secure Socket Layer (SSL) or Transport Layer Security (TLS) together with HTTP. SSL/TLS encrypts the HTTP communication by an asynchronous private/public encryption. Therefore, the server has a certificate that contains the private and public key and is hardly wired with the domain it serves. That means a device in a local network is not meant to act as a HTTPS server as long as it is not routable by the internet with a domain.

Some routers (such as my fritzbox) have the option to generate an HTTPS certificate. That is possible because the router-vendor (AVM) offers domains for all of their routers and the "Let’s Encrypt" initiative offers free signed certificates for endusers. Generating a certificate for the router allows the router to act as an SSL proxy similar to port forwarding. The client talk to the router by encrypted HTTPS and the router talks to the Raspi by decrypted HTTP.

That worked fine, but all in all that approach is a massive security issue for my local network. I don't want to open my entertainment server and smart home applications to the World Wide Web. Even with HTTPS anyone could connect to the Raspi and execute zero-day-exploits on the running applications. You see the irony of securing by forcing to HTTPS?

And the whole point of running a Raspi is to be a bit less dependent to the internet connection.

Step 5: DNS Rerouting (winner!)

But did you see the network_security_config.xml exceptions? I think not all of them are really necessary for Garmin App.
For example garmin.cn looks like it is meant for asia. And garmincdn.com can't be even resolved. Wouldn't it be nice to recycle one of those URLs and redirect them to any ip-address in the local network?

pi-hole https://pi-hole.net/ is a great project that implements a DNS server. A DNS (Domain Name Service) server resolves domain names to the correct ip-address. The purpose of pi-hole is to block advertisement domains by a DNS sinkhole. A black hole for ads.

And one key-feature of pi-hole are the Local DNS Records. These records allow you to manually enter a domain and the corresponding ip. This allows me to redirect garmin.cn to the Raspis ip address. Since the network_security_config.xml exceptions also apply for subdomains, you could also create your own domain like myraspi.garmin.com. One important point here is to consider the DNS-Rebind protection. Some routers don't suppress DNS resolves with local ip addresses. But that's exactly what I want, so I added the domain to a DNS-Rebind protection exception list on my router.

And voilà, my watch app finally communicates to the Raspi by HTTP with a rerouted URL. This approach is very stable, does not introduce security issues and should work for android and iOS. Furthermore, my local network is now free of ads and sides load way faster now because of the great work of pi-hole.

Step 6: Conclusion

If you own a Garmin Watch and want to do some Smart Home or IoT projects, this post can save you a lot of time and frustration.
But, nonetheless, I was really happy to find a good solution for my situation. Of course this solution does not scale to normal users of a Garmin Watch. Feels like the real cause of the issue is bad luck of android's network-security-config whitelist not supporting subnets (like 192.168.0.0/16) in combination with Garmin using that feature. And that's a shame as Garmin has a nice SDK that lets you easily develop apps for the Watch.