Sinkhole LG webOS telemetry with UniFi alone (no Pi-hole, no rooting) - via the official Integration API

Sinkhole LG webOS telemetry with UniFi alone (no Pi-hole, no rooting)

Transparency up front: this was done pair-style with an AI agent (Claude). It drove the gateway captures, wrote the script and drafted this write-up. Flagged prominently because you should be suspicious of AI-generated domain lists: that is exactly why every number here comes from an actual packet capture (not from the model's training data), why the sinkhole was verified with dig against a real controller, and why this README documents how to reproduce the capture instead of trusting the list.

After the Gamers Nexus investigation ("216,000,000 Spy TVs", Sep 2026) the usual advice for blocking LG TV telemetry is "get a Pi-hole/AdGuard" or the Level1Techs mini-howto (which assumes an upstream DNS blocker or a rooted TV). If your network is a UniFi gateway (UDM / UDM-SE / UDR), you don't need extra hardware: the official Network Integration API has a DNS-records endpoint (/v1/sites/{siteId}/dns/policies), and an A record pointing at 0.0.0.0 is a DNS sinkhole. unifi-lg-sinkhole.sh automates it - idempotent, plain curl/jq, API key from Settings → Control Plane → Integrations.

The built-in Ad Blocking does not cover LG's own domains (verified: it blocked generic adtech like tag.aticdn.net while DE.ibs.nextlgsdp.com went straight through), and Domain Filtering is not exposed via any API.

How to run it

You do not SSH into the gateway and nothing gets installed on it. The script only talks to your controller's HTTPS API and creates ordinary DNS records you can inspect and delete in the UI at any time.

  1. Run it from any Linux/macOS machine (or WSL) on your LAN that has curl, jq and dig.
  2. Create an API key in the UniFi UI: Settings → Control Plane → Integrations → Create API Key.
  3. Find your site id: curl -sk -H "X-API-KEY: $UNIFI_API_KEY" https:///proxy/network/integration/v1/sites | jq '.data[]|{id,name}'
  4. Run it:
    export UNIFI_HOST=https://   # default: https://192.168.1.1
    export UNIFI_API_KEY=...
    export SITE_ID=...
    VERIFY_RESOLVER= ./unifi-lg-sinkhole.sh
    
  5. Re-run any time - it is idempotent (existing records are skipped). Rollback: delete the records in Settings → Policy Table, filtered on the "DNS Records" preset (.../network/default/settings/policy-table?preset=dns-records).

The full domain list

For the suspicious among us (rightly so): this is the complete, expanded list the script would create, generated from the script's own arrays, so you can audit it or type the entries into the Policy Table manually. All records are A records pointing at 0.0.0.0, TTL 3600. Fair warning: the region-prefixed entries are exactly the tedious part the script exists for.

Default tier: ads / tracking / ACR (55 entries)

*.alphonso.tv
*.lgsmartad.com
ad.lgappstv.com
aic.cdpbeacon.lgtvcommon.com
aic.cdpsvc.lgtvcommon.com
aic.homeprv.lgtvcommon.com
aic.nudge.lgtvcommon.com
aic.rdl.lgtvcommon.com
aic.rdx2.lgtvsdp.com
aic.rdx2.nextlgsdp.com
alphonso.tv
cdpbeacon.lgtvcommon.com
cdpsvc.lgtvcommon.com
de.cdpbeacon.lgtvcommon.com
de.cdpsvc.lgtvcommon.com
de.homeprv.lgtvcommon.com
de.nudge.lgtvcommon.com
de.rdl.lgtvcommon.com
de.rdx2.lgtvsdp.com
de.rdx2.nextlgsdp.com
eic.cdpbeacon.lgtvcommon.com
eic.cdpsvc.lgtvcommon.com
eic.homeprv.lgtvcommon.com
eic.nudge.lgtvcommon.com
eic.rdl.lgtvcommon.com
eic.rdx2.lgtvsdp.com
eic.rdx2.nextlgsdp.com
eu.cdpbeacon.lgtvcommon.com
eu.cdpsvc.lgtvcommon.com
eu.homeprv.lgtvcommon.com
eu.nudge.lgtvcommon.com
eu.rdl.lgtvcommon.com
eu.rdx2.lgtvsdp.com
eu.rdx2.nextlgsdp.com
homeprv.lgtvcommon.com
jp.cdpbeacon.lgtvcommon.com
jp.cdpsvc.lgtvcommon.com
jp.homeprv.lgtvcommon.com
jp.nudge.lgtvcommon.com
jp.rdl.lgtvcommon.com
jp.rdx2.lgtvsdp.com
jp.rdx2.nextlgsdp.com
lgsmartad.com
nudge.lgtvcommon.com
rdl.lgtvcommon.com
rdx2.lgtvsdp.com
rdx2.nextlgsdp.com
smartshare.lgtvsdp.com
us.cdpbeacon.lgtvcommon.com
us.cdpsvc.lgtvcommon.com
us.homeprv.lgtvcommon.com
us.nudge.lgtvcommon.com
us.rdl.lgtvcommon.com
us.rdx2.lgtvsdp.com
us.rdx2.nextlgsdp.com

Broadcast tier (INCLUDE_BROADCAST=1): HbbTV audience measurement (8 entries)

*.nuid.nmrodam.com
*.trk.sensic.net
cdn-gl.nmrodam.com
de-config-preproduction.sensic.net
de1preprod-s2s.sensic.net
scheduler.hbbtv.smartclip.net
secure-eu.nmrodam.com
tr.technical-service.net

Aggressive tier (INCLUDE_AGGRESSIVE=1): billing/platform-adjacent, see maintainer feedback below (22 entries)

aic.ibs.lgappstv.com
aic.ibs.nextlgsdp.com
aic.tv.wiselg.com
de.ibs.lgappstv.com
de.ibs.nextlgsdp.com
de.tv.wiselg.com
eic.ibs.lgappstv.com
eic.ibs.nextlgsdp.com
eic.tv.wiselg.com
eu.ibs.lgappstv.com
eu.ibs.nextlgsdp.com
eu.tv.wiselg.com
ibis.lgappstv.com
ibs.lgappstv.com
ibs.nextlgsdp.com
jp.ibs.lgappstv.com
jp.ibs.nextlgsdp.com
jp.tv.wiselg.com
tv.wiselg.com
us.ibs.lgappstv.com
us.ibs.nextlgsdp.com
us.tv.wiselg.com

What makes this list different

It is based on a wire capture of an actual EU device (OLED55C17LB, webOS 6.0, DE region, 40-min DNS+SNI capture on the gateway, Sep 2026), not on copied US lists. Key findings:

  • The current domain family is nextlgsdp.com (successor of lgtvsdp.com), with region prefixes like DE. / EIC.. Our device also contacted .ibs.nextlgsdp.com and tv.wiselg.com - see the maintainer-feedback update below for why those are NOT in the default tier. The widely-copied Perflyst-era lists (lgsmartad.com, lgappstv.com, alphonso.tv) were never queried once by this device
    • if you only block those, you block nothing.
  • Wildcard A records work, but the provisioning delay will gaslight you (correction of this gist's first version, which claimed they don't): new records take up to ~45 s to reach dnsmasq, and a dig before that returns stale upstream answers. Verified A/B with a probe record: after the wait, any subdomain resolves to the sinkhole; after deleting the record, it doesn't. The script uses wildcards only for wholly-blockable families (*.lgsmartad.com, *.alphonso.tv, hashed tracker subfamilies) and keeps mixed families enumerated - see next point for why.
  • Do NOT block the firmware path: *gfts* hosts (NG File Transfer Service) exist on lge.com, nextlgsdp.com and tv.wiselg.com (e.g. eic-ngfts.lge.com, eic-gfts.nextlgsdp.com), plus snu.lge.com (update notification). With unpatched webOS RCEs under responsible disclosure, a blocked update is worse than telemetry. The script hard-refuses to run if such a host slips into the list.
  • No bypass observed: all DNS went to the gateway; no DoH, no DoT, no QUIC, no hardcoded resolvers (this device, this firmware - verify yours).
  • The platform core (DE.nextlgsdp.com), wiseconfig.lgtvcommon.com and lgtviot.com (ThinQ) are deliberately left reachable - blocking the SDP core is where "non-breaking" ends.
  • Optional (INCLUDE_BROADCAST=1): HbbTV audience measurement seen while watching German linear TV - RTL (tr.technical-service.net), GfK (sensic.net), Nielsen (nmrodam.com), smartclip's ad scheduler. App assets stay unblocked so red-button apps keep working.

Update: maintainer feedback (tiers)

After feedback from the hagezi maintainer two domains moved out of the default list into an aggressive tier (INCLUDE_AGGRESSIVE=1): ibs.* (lgappstv.com/nextlgsdp.com) is in-app billing infrastructure, not an ad beacon, and tv.wiselg.com hosts an NGFTS mirror (app updates / Content Store), so the zone carries functional traffic; what eic.tv.wiselg.com itself serves is unpublished. Both were contacted by our fully-opted-out device, which is why they showed up in the capture - but "phones home" and "ad/tracking" are not the same claim. Enable the aggressive tier only for a TV that never uses apps, the Content Store or any paid service (ours doesn't; it runs fine with them blocked). Community data points since publishing: blocking the SDP core (IN.nextlgsdp.com) breaks YouTube content loading, confirming the backbone classification; and a webOS-5-era CX still queries prov-lg.alphonso.tv daily, so keep the alphonso wildcard even though newer devices drop that family.

Honest limits

  • Records are site-wide (all VLANs). The default list is LG-only, so that's fine; think before adding broader adtech domains.
  • GfK/Nielsen use per-device hashed subdomains (.trk.sensic.net, .nuid.nmrodam.com) - covered here via wildcard records on those subfamilies. For full curated-list blocking across all vendors you still want Pi-hole/AdGuard (e.g. with the hagezi webOS list).
  • New records take up to ~45 s to reach dnsmasq (the script waits before verifying - do the same before trusting any manual dig).
  • Data basis is one device / one firmware / EU region, opted out of everything. Different models and regions will differ - ideally verify with your own capture and please contribute domains upstream (hagezi#11474).

Prefer doing this with an AI agent instead of a script?

The DNS-records endpoint used here was found by querying the official UniFi API docs through mcp-unifi-applications - an MCP server that makes UniFi's API documentation searchable for LLMs (read-only, no controller credentials). Point your agent at it and ask it to build/adapt the sinkhole for your site instead of running this script blindly.

DNS is one layer, not the whole answer

Sinkholing complements, not replaces: put the TV in an IoT VLAN with default-deny towards your other segments (that alone kills the LAN-enumeration issue), decline the user agreements on the device, and keep firmware updates flowing. Rollback: delete the records in Settings → Policy Table (filter: DNS Records), or via DELETE /dns/policies/{id}.

#!/usr/bin/env bash
# DNS-sinkhole LG webOS telemetry domains on a UniFi gateway (UDM/UDM-SE/UDR)
# using ONLY the official Network Integration API - no Pi-hole, no rooting,
# no SSH on the gateway. Creates A records pointing at 0.0.0.0.
#
# Requirements: curl, jq, dig. An API key from the UniFi UI
# (Settings -> Control Plane -> Integrations -> API Key).
#
# Usage:
# export UNIFI_API_KEY=...
# ./unifi-lg-sinkhole.sh # safe tier: ads/tracking/ACR only
# INCLUDE_BROADCAST=1 ./unifi-lg-sinkhole.sh # + HbbTV audience measurement
# # (GfK/Nielsen/RTL)
# INCLUDE_AGGRESSIVE=1 ./unifi-lg-sinkhole.sh # + billing/platform-adjacent
# # hosts; ONLY for TVs that
# # never use apps/store/billing
#
# Find your site id:
# curl -sk -H "X-API-KEY: $UNIFI_API_KEY" \
# "$UNIFI_HOST/proxy/network/integration/v1/sites" | jq '.data[]|{id,name}'
#
# KNOWN LIMITS (measured 2026-09, see README):
# - Wildcard records ("*.domain") DO work - but only after the
# provisioning delay (see below). Correction of an earlier version of
# this gist which claimed they don't: that was a verify running too
# early. Wildcards are used here ONLY for families that are wholly
# blockable (no update/functional host underneath); mixed families
# stay enumerated because LG's firmware-transfer (gfts) hosts live on
# several of them.
# - Records are SITE-WIDE (all VLANs/clients). The default list is
# LG-specific so collateral damage is nil; the broadcast list is
# tracker-only but shared with browsers (still fine, they are trackers).
# - New records reach dnsmasq after a provisioning delay (belegt < 45 s;
# a too-early dig shows stale upstream answers and WILL mislead you).
set -euo pipefail
: "${UNIFI_HOST:=https://192.168.1.1}"
: "${UNIFI_API_KEY:?export UNIFI_API_KEY=}"
: "${SITE_ID:?export SITE_ID=}"
: "${SINKHOLE_IP:=0.0.0.0}"
: "${INCLUDE_BROADCAST:=0}"
: "${VERIFY_RESOLVER:=}" # e.g. 192.168.1.1; empty = skip verification
API="$UNIFI_HOST/proxy/network/integration/v1/sites/$SITE_ID/dns/policies"
api() { curl -sk -H "X-API-KEY: $UNIFI_API_KEY" "$@"; }
# ---------------------------------------------------------------------------
# Domain list. Sources:
# - Gamers Nexus "216,000,000 Spy TVs" indicator table (2026-09-06)
# - Perflyst SmartTV.txt (regional-prefix pattern: us./jp./aic. ...)
# - Own 40-min DNS+SNI capture of an EU OLED55C17LB (webOS 6.0, 2026-09-15):
# the CURRENT domain family is nextlgsdp.com (DE./EIC. prefixes) and
# tv.wiselg.com - the older US lists miss it entirely.
# Deliberately NOT blocked (platform still works, updates keep flowing):
# - *gfts* hosts = firmware transfer, exists on lge.com AND nextlgsdp.com
# AND tv.wiselg.com. With unpatched RCEs in the wild, a blocked firmware
# update is a bigger risk than telemetry.
# - snu.lge.com family = update notification.
# - The SDP core (DE.nextlgsdp.com), wiseconfig.lgtvcommon.com,
# lgtviot.com = platform/config/ThinQ control paths.
# ---------------------------------------------------------------------------
REGIONS=(us eu de aic jp eic)
# Wildcards: ONLY families with no update/functional host underneath.
# Do NOT add e.g. *.nextlgsdp.com or *.tv.wiselg.com - both carry gfts
# firmware-transfer hosts.
WILDCARD_HOSTS=(
'*.lgsmartad.com' # LG Ad Solutions
'*.alphonso.tv' # ACR (LG Ads)
)
REGIONAL_HOSTS=(
rdx2.lgtvsdp.com # legacy ACR path
rdx2.nextlgsdp.com # CURRENT ACR (webOS 5/6)
cdpbeacon.lgtvcommon.com cdpsvc.lgtvcommon.com # beacons
nudge.lgtvcommon.com homeprv.lgtvcommon.com rdl.lgtvcommon.com
)
BASE_HOSTS=(
lgsmartad.com # apex; wildcard skips apex
ad.lgappstv.com
rdx2.lgtvsdp.com smartshare.lgtvsdp.com
rdx2.nextlgsdp.com
cdpbeacon.lgtvcommon.com cdpsvc.lgtvcommon.com nudge.lgtvcommon.com
homeprv.lgtvcommon.com rdl.lgtvcommon.com
alphonso.tv # apex; wildcard skips apex
)
# AGGRESSIVE tier (INCLUDE_AGGRESSIVE=1): contacted by our device even with
# everything opted out, but per blocklist-maintainer feedback (hagezi,
# dns-blocklists#11474) these are billing/platform infrastructure, not
# ad/tracking: ibs.* = in-app billing; tv.wiselg.com hosts an NGFTS mirror
# (app updates / Content Store). Only enable if the TV NEVER uses apps,
# the Content Store or any paid service - breakage risk is real otherwise.
AGGRESSIVE_REGIONAL=(ibs.lgappstv.com ibs.nextlgsdp.com tv.wiselg.com)
AGGRESSIVE_BASE=(ibs.lgappstv.com ibis.lgappstv.com ibs.nextlgsdp.com tv.wiselg.com)
# HbbTV / broadcast audience measurement, observed while watching ZDF + RTL
# (German linear TV). Trackers only - app assets are intentionally left
# alone so red-button apps keep working.
BROADCAST_HOSTS=(
tr.technical-service.net # RTL HbbTV tracking
scheduler.hbbtv.smartclip.net # smartclip ad scheduler
de-config-preproduction.sensic.net de1preprod-s2s.sensic.net # GfK
'*.trk.sensic.net' # GfK per-device hashed beacons
secure-eu.nmrodam.com cdn-gl.nmrodam.com # Nielsen
'*.nuid.nmrodam.com' # Nielsen per-device hashed beacons
)
DOMAINS=("${WILDCARD_HOSTS[@]}" "${BASE_HOSTS[@]}")
for r in "${REGIONS[@]}"; do
for h in "${REGIONAL_HOSTS[@]}"; do DOMAINS+=("$r.$h"); done
done
if [ "${INCLUDE_AGGRESSIVE:-0}" = "1" ]; then
DOMAINS+=("${AGGRESSIVE_BASE[@]}")
for r in "${REGIONS[@]}"; do
for h in "${AGGRESSIVE_REGIONAL[@]}"; do DOMAINS+=("$r.$h"); done
done
fi
[ "$INCLUDE_BROADCAST" = "1" ] && DOMAINS+=("${BROADCAST_HOSTS[@]}")
mapfile -t DOMAINS < <(printf '%s\n' "${DOMAINS[@]}" | sort -u)
# Guard: never let a firmware-update host into the list, on ANY family.
for d in "${DOMAINS[@]}"; do
case "$d" in
*gfts*|*.lge.com|lge.com)
echo "REFUSING '$d': firmware-update path must stay reachable" >&2
exit 1;;
esac
done
echo "Target: ${#DOMAINS[@]} domains -> $SINKHOLE_IP"
# Fetch existing A records (endpoint is PAGINATED - read every page or you
# will re-POST existing records and get 400 record-already-exists).
total=$(api "$API?offset=0&limit=1" | jq -r .totalCount)
existing=$(for ((o = 0; o < total; o += 200)); do
api "$API?offset=$o&limit=200" |
jq -r '.data[] | select(.type=="A_RECORD") | .domain'
done)
created=0 skipped=0
for d in "${DOMAINS[@]}"; do
if grep -qixF "$d" <<<"$existing"; then
skipped=$((skipped + 1))
continue
fi
code=$(api -o /dev/null -w '%{http_code}' -X POST "$API" \
-H 'Content-Type: application/json' \
-d "{\"type\":\"A_RECORD\",\"enabled\":true,\"domain\":\"$d\",\"ipv4Address\":\"$SINKHOLE_IP\",\"ttlSeconds\":3600}")
if [ "$code" = 200 ] || [ "$code" = 201 ]; then
created=$((created + 1))
else
echo "WARN: POST $d -> HTTP $code" >&2
fi
done
echo "Done: $created created, $skipped already present."
if [ -n "$VERIFY_RESOLVER" ]; then
[ "$created" -gt 0 ] && { echo "Waiting 45 s for provisioning..."; sleep 45; }
echo "--- verification (@$VERIFY_RESOLVER) ---"
for probe in ad.lgappstv.com de.ibs.nextlgsdp.com "wildcardtest-$RANDOM.lgsmartad.com"; do
echo "$probe -> $(dig +short "$probe" "@$VERIFY_RESOLVER" | tr '\n' ' ') (expect $SINKHOLE_IP)"
done
echo "snu.lge.com -> $(dig +short snu.lge.com "@$VERIFY_RESOLVER" | tr '\n' ' ') (expect REAL address - update path!)"
fi
添加评论
点赞收藏
点踩分享查看原文
评论
?
参与讨论