Flash OpenWRT to NanoPi R5S without EFlasher

Recently bought a FriendlyARM NanoPi R5S with 4GB RAM to replace the router and firewall functions of GL.iNet GL-MT6000 (Flint 2) that has 1GB RAM only. FriendlyARM provides FriendlyWRT official image, a fork of OpenWRT. I prefer OpenWRT official image for a vanilla experience.

The easiest way to flash OpenWRT is to use EFlasher utility provided by FriendlyARM, but dd actually works just fine. I found this R6S post to be useful as the guide is also applicable to R5S.

Download and flash OpenWRT to SD card.

curl -LO "https://downloads.openwrt.org/releases/25.12.6/targets/rockchip/armv8/openwrt-25.12.6-rockchip-armv8-friendlyarm_nanopi-r5s-ext4-sysupgrade.img.gz"
gunzip -c "openwrt-25.12.6-rockchip-armv8-friendlyarm_nanopi-r5s-ext4-sysupgrade.img.gz" | sudo dd of=/dev/sdaX bs=1m

Insert the SD card to R5S to boot up OpenWRT. SSH into R5S, download and flash OpenWRT to eMMC (“/dev/mmcblk1”). SD card is identified as “/dev/mmcblk0” in OpenWRT.

curl -LO "https://downloads.openwrt.org/releases/25.12.6/targets/rockchip/armv8/openwrt-25.12.6-rockchip-armv8-friendlyarm_nanopi-r5s-ext4-sysupgrade.img.gz"
gunzip -c "openwrt-25.12.6-rockchip-armv8-friendlyarm_nanopi-r5s-ext4-sysupgrade.img.gz" | sudo dd of=/dev/mmcblk1 bs=1m

df -h shows the eMMC root partition (/dev/mmcblk1p2) is only ~150MB not 16GB, so the partition needs to be expanded. resize2fs may prompt to fix partition alignment, proceed with it.

“/dev/mmcblk1p1” is the boot partition, leave it as is.

apk update
apk add parted resize2fs
resize2fs /dev/mmcblk1p2

df -h should now show the root partition is 16GB.

Remove the SD card and reboot. R5S should now boot OpenWRT from the eMMC.

Compiling a package from a forked openwrt/packages

Recently got first pull request to update kitty-terminfo package openwrt/packages#30876 merged. This is a guide on how I compiled kitty-terminfo.

Before compiling the package, I’ve already forked openwrt/packages repo to curbengh/openwrt-packages, created “kitty” branch and pushed the commit to the branch.

Let’s start by cloning the “openwrt/openwrt” repo and checkout the latest stable branch.

# https://openwrt.org/docs/guide-developer/toolchain/use-buildsystem
git clone https://github.com/openwrt/openwrt.git
cd openwrt
git checkout v25.12.6

Update the “packages” line to point to the commit of forked repo, in my case, it’s https://github.com/curbengh/openwrt-packages.git^ba7792c8d2396eb23ec6bcf5a20876525aa3f7b8

nano feeds.conf.default
  src-git packages https://github.com/curbengh/openwrt-packages.git^commit-hash

Update and install feeds

./scripts/feeds update -a
./scripts/feeds install -a

Select a device and the relevant package to build. “Target images” can be deselected to skip building device image. Refer to firmware-selector page to find out target and subtarget name.

make menuconfig
  Target System: MediaTek ARM
  Subtarget: Filelogic
  Target Profile: Xiaomi Mi Router AX3000T
  Target Images: deselect

  scroll to bottom > Utilities > Terminal > kitty-terminfo
  Libraries > libncurses (automatically selected, optional if current dependency version is ok)

make defconfig download clean world

Install build tools

# https://openwrt.org/docs/guide-developer/toolchain/single.package
make tools/install
make toolchain/install

Finally, compile the package.

make package/ncurses/compile
make package/kitty-terminfo/compile
make package/index

Compiled package is available in the “bin/“ folder.

Monitor application traffic using Proxifier and Burp Suite

Initially discovered Proxifier from a SANS article. Since then, I’ve been using it to troubleshoot connection issue in corporate proxy environment. Previously Wireshark was used for troubleshooting, but it captures the whole operating system (OS) making it challenging to discern a specific app. Similar issue with Burp Suite when using OS-wide proxy configuration, app-specific proxy requires the app to support proxy connection. By using Proxifier, I can target a specific app only, resulting in accurate traffic analysis.

  1. In Burp Suite, Proxy top tab > proxy settings > import/export CA certificate.
  2. Export “Certificate and private key in PKCS#12 keystore”, set filename as burp.pfx with simple password.
  3. Open the exported PFX file using default Windows Certificate utility, set “current user”, enter password set in Step 2, “Place all certificates in the following store” > “Trusted Root Certificate Authorities”.
  4. In Proxifier, create a new proxy configuration:
    • Address: 127.0.0.1
    • Port: 8080
    • Protocol: HTTPS
  5. When prompted,
    • Use this proxy by default: no
    • Edit Proxification Rules now: yes
  6. Profile > Proxification Rules, create a new rule:
    • Applications: “application.exe”
    • Action: “Proxy HTTPS 127.0.0.1”
    • Ensure rule is above Default, move above Localhost rule to capture localhost traffic
  7. Back in Burp Suite, Proxy top tab > HTTP history tab, application traffic will show up here.

GoodWe ESA ESS Network Requirements

Disclaimer: Unofficial article, use at your own risk.

DNS queries and IP+port connections are monitored through OpenWRT dnsmasq and conntrack (NAT table).

SEMS+ iOS app:

  • ali-stats.jpush.cn*
  • app-analytics-services.com*
  • app-measurement.com*
  • au-gateway.semsportal.com
  • au.mqtt.goodwe-power.com
  • ce3e75d5.jpush.cn*
  • clients3.google.com
  • config.jpush.cn*
  • firebaseinstallations.googleapis.com
  • firebaselogging-pa.googleapis.com
  • firebaseremoteconfig.googleapis.com
  • firebase-settings.crashlytics.com
  • gd-stats.jpush.cn*
  • hk-gateway.semsportal.com
  • o.alicdn.com
  • oversea-gops-hk.oss-cn-hongkong.aliyuncs.com
  • semsplus.goodwe.com
  • semsplus.oss-cn-hongkong.aliyuncs.com
  • sentry1.semsportal.com
  • sentry.io
  • sis.jpush.io*
  • status-ipv6.jpush.cn*
  • tsis.jpush.cn*

*Optional domains, commonly blocked by adblocker.

SEMS+ Web portal:

  • a1320.dscb.akamai.net
  • au-gateway.semsportal.com
  • au-semsplus.goodwe.com
  • cloudauth-device-dualstack.cn-shanghai.aliyuncs.com
  • g.alicdn.com
  • netty-wss-au.iot.goodwe-power.com:8885
  • o.alicdn.com
  • o.alicdn.com.w.cdngslb.com
  • popunify-large-dualstack-1.cn-shanghai.aliyuncs.com.vipgds.alibabadns.com
  • semsplus.oss-accelerate.aliyuncs.com
  • semsplus.oss-cn-hongkong.aliyuncs.com

If browser setting has WebGL enabled but NoScript is configured to restrict it, configure NoScript to allow WebGL on au-semsplus.goodwe.com, otherwise the page freezes in less than a minute. The portal actually works fine without WebGL, but it has to be disabled in the browser setting, which disables for every websites.

GoodWe ESA connected to wifi using WiFi/LAN Kit-20 dongle:

Hostname: GW_WIFILAN_2
MAC address: C0:CD:D6:xx:xx:xx (Espressif)

  • broker.goodwe-power.com:8883
  • iot-cn.oss-cn-hangzhou.aliyuncs.com:443
  • iot.goodwe-power.com:80
  • jiankong-hk.oss-accelerate.aliyuncs.com:80
  • www.goodwe.com (icmp)

There’s also DNS traffic to OpenDNS where router can intercept using port forwarding rule.

IPProtocolPort
208.67.222.222udp53

Xiaomi Mijia Mi Robot Vacuum-Mop 2 (MJST1S) Network Requirements

Disclaimer: Unofficial article, use at your own risk.

DNS queries and IP+port connections are monitored through OpenWRT dnsmasq and conntrack (NAT table).

Initial wifi SSID: ijai-vacuum-v2_miapxxxx
Hostname: rk3308_robot32
MAC address: 58:B6:23:xx:xx:xx (Beijing Xiaomi Mobile Software)
Xiaomi Home server: Chinese mainland

Mi Robot Vacuum-Mop 2:

  • cnbj2.fds.api.xiaomi.com:443
  • dlg.io.mi.com
  • ot.io.mi.com:8053*
  • ots.io.mi.com:443

Also these IPs without corresponding domains,

IPProtocolPort
36.156.49.91udp80
110.43.0.83tcp443
120.92.158.163tcp443
124.251.34.212tcp443
39.101.90.191udp8053*
120.52.181.238udp8053*
120.52.181.239udp8053*
120.92.146.6udp8053*
120.92.65.237udp8053*
120.92.65.254udp8053*
121.228.168.18udp8053*
123.125.102.216udp8053*
124.251.101.16udp8053*
220.181.106.199udp8053*
220.181.106.200udp8053*

*Optional port, I don’t notice any issue after blocking it.

Xiaomi Home Android app:

  • account.xiaomi.com
  • api.account.xiaomi.com
  • api.device.xiaomi.net
  • api.mijia.tech
  • api.miwifi.com
  • apm-rum.inf.miui.com*
  • app.chat.global.xiaomi.net
  • cdn.cnbj0.fds.api.mi-img.com
  • cdn.cnbj1.fds.api.mi-img.com
  • cdn.web-global.fds.api.mi-img.com
  • chat.kefu.mi.com
  • cnbj1.fds.api.xiaomi.com
  • core.api.mijia.tech
  • data.sec.miui.com*
  • firebase.googleapis.com
  • firebaseinappmessaging.googleapis.com
  • firebaseinstallations.googleapis.com
  • firebaselogging.googleapis.com
  • font.sec.miui.com
  • graph.facebook.com
  • home.mi.com
  • i.ai.mi.com
  • i.huodong.mi.com
  • iot-cdn0.io.mi.com
  • iot-cdn.io.mi.com
  • mcfe–account-static-legacy.cnbj1.mi-fds.com
  • m.mi.com
  • register.xmpush.global.xiaomi.com
  • resolver.msg.global.xiaomi.net
  • sdkconfig.xiaomi.com
  • shopapi.io.mi.com
  • ssl-cdn.static.browser.mi-img.com
  • static-verify.sec.xiaomi.com
  • stream.api.mijia.tech
  • sts.api.mijia.tech
  • stun.services.mozilla.com
  • tracking.miui.com*
  • tsmapi.pay.xiaomi.com
  • verify.sec.xiaomi.com
  • www.google-analytics.com*
  • www.googletagmanager.com

*Optional domains, commonly blocked by adblocker.

Framework Laptop 13 USB not working (solved)

The far left USB-C expansion card of my Framework Laptop 13 with AMD AI 300 suddenly stopped working after wake from suspend. Keyboard stopped working and couldn’t charge through that port. I took out the expansion card and plugged in to the near left USB-C expansion card (so two expansion cards) then plugged in my keyboard, that worked just fine so that expansion card was still functional. Then, I tried charging through the far left port directly without a USB-C expansion card, but the charging indicator light didn’t light up.

I searched around the web and tried this suggestion. I powered it off and unplugged charger, waited around 2 minutes and switched back on. Keyboard and charging then started to work on the far left port with expansion card again. I didn’t have to disconnect the battery

*.cdn.cloudflare.net CNAME for split DNS situation

Cloudflare offers a nifty shortcut for split DNS situation. Company may share the same domain name for their public and internal domain. In the internal domain, some subdomains can point to internal IP address that are not available on public DNS records. Due to separation of DNS nameserver, DNS admin often need to replicate any creation of public subdomains into internal nameserver including subsequent update of values.

Instead of replicating every public DNS records (A/AAAA/CNAME/MX/TXT/etc) of a subdomain, simply create a CNAME record that point to <fqdn>.cdn.cloudflare.net in the internal nameserver. This works with subdomains that have proxy mode enabled and also those without.

Internal nameserver:

DomainTypeValue
a.example.comCNAMEa.example.com.cdn.cloudflare.net

Using Cloudflare Origin certificate on Salesforce

As certificate validity is now reducing to 200 days in 2026, Salesforce administrators must find a practical way to manage certificate renewal. Ideally, Salesforce should support ACME protocol to automate renewal.

For those utilising Cloudflare CDN, it is possible to install Cloudflare Origin certificate with 15-year validity on Salesforce. Although Salesforce documentation mentions the certificate must be CA-signed, self-signed certificate including Cloudflare Origin certificate (which is not public CA-signed) is actually acceptable.

  1. In Cloudflare, change the encryption mode to “Full (strict)”.
  2. Enable True-Client-IP if on Enterprise plan.
  3. Add CNAME record (if not exist) that points to the Salesforce site, e.g. custom-domain.com.xxx.live.siteforce.com. Ensure the CNAME record is in Proxied/orange-clouded mode.
  4. In Salesforce, generate and download certificate signing request (CSR).
  5. In Cloudflare, create a new origin certificate, choose “Use my private key and CSR”, paste the CSR value, update hostname and Create it.
  6. Copy the PEM value then append Origin CA root certificate and save it with “.pem” extension. The file should have both leaf and root certificates.
  7. Upload the “.pem” file to Salesforce.
  8. Select the new certificate in Salesforce domain configuration.

In the last step, do not select the “Use a third-party service or CDN” option despite Cloudflare being a CDN service. That option will configure the Salesforce site to serve over HTTP, requiring “Flexible” encryption mode in Cloudflare which is not ideal.

Change M365 Bookings calendar sender address

TL;DR Update UPN and wait at least 24 hours.

During setup of M365 Bookings shared calendar, a company account is created with user principal name (UPN) in the format of [email protected]. This UPN is used as the sender address when sending out booking notifications. The issue is that *.onmicrosoft.com email domain is often abused for spams and many email providers (notably iCloud) are starting to reject any incoming email with that domain. Microsoft meanwhile has started to throttle usage of that domain for sending email.

There are two ways to update the sender address:

  1. OWA mailbox policy
  2. Change UPN

Method (1) is the Microsoft’s recommended method, but I prefer Method (2) due to visibility on Entra ID; anyone with read-only account within the tenant can view the updated UPN in Entra ID whereas only M365 admin can view mailbox policy. If a subdomain (e.g. [email protected]) is preferred over the company’s email domain (example.com), the subdomain will need to be added to M365 with MX (booking-example-com.mail.protection.outlook.com), SPF ("v=spf1 include:spf.protection.outlook.com -all") and DKIM DNS records, regardless of method (1) or (2).

If the booking calendar is embedded in the company website, the iframe link https://outlook.office365.com/owa/calendar/[email protected]/bookings/ needs to be updated to https://outlook.office365.com/owa/calendar/<new-upn>/bookings/.

Once UPN is updated, it may take at least a day for the change to apply. In my experience, the new UPN is applied to new appointment’s notification within hours, but it only applied to cancellation notification after 24 hours (or longer).

Opening a web browser from Java apps in WSL

Linux CLI tools typically rely on xdg-open or fallback to $BROWSER environment variable to open the default browser, usually for OAuth authentication to authorise the CLI tool without using a permanent credential. Java apps behave differently by trying common browser executables in $PATH if the default browser could not be located. To launch a web browser from a Java app in WSL, simply add a symlink in “/usr/bin/“ that points to the browser executable in Windows, this way you don’t have to install a web browser in WSL.

# Examples, just choose one.
sudo ln -s "/mnt/c/Program Files (x86)/Microsoft/Edge/Application/msedge.exe" "/usr/bin/msedge"
sudo ln -s "/mnt/c/Program Files/Google/Chrome/Application/chrome.exe" "/usr/bin/chrome"
sudo ln -s "/mnt/c/Program Files/Mozilla Firefox/firefox.exe" /usr/bin/firefox"

Programs that rely on OAuth authentication typically listens on a random port to receive credential during the last step of authorisation. Since WSL enables IPv6 by default, the program may listen on IPv6 only. I noticed this behaviour when I check for listening ports netstat -aon | findstr ":port-number" on Windows and the output was [::1]:port-number without any 127.0.0.1. This can cause the program to unable to receive credential and fail authorisation if the Windows host has IPv6 disabled. A workaround for this issue is to also disable IPv6 on WSL to force the program to listen on IPv4. Append this line to “$home\.wslconfig” in Windows and restart WSL wsl --shutdown,

[wsl2]
kernelCommandLine=ipv6.disable=1

Credit