Tagged in: projects, dns, networking

My networking setup at home is now perfectlyTM working, and I’m now dealing with annoyances. Those annoyances include advertisement on services I already pay for (Amazon Prime and M6+).

§ M6+

M6+ is the current name of the VOD service from the M6 group. That group holds diffusion rights in France for Lego Masters, which is a fun and interesting show. Too bad M6 litters the episodes with ads (1 every 10 or so minutes).

I’m using the UHD TV decoder to access M6+, and it has not been a smooth experience.

§ adding the UHD TV decoder to the network

First things first: connect the TV decoder to the network and stop it from complaining.

The TV box lives in its own VLAN (vlan52). And it needs to be sent some specific DHCP options to accept the lease.

# TV
subnet 10.207.52.1 netmask 255.255.255.0 {
  option routers 10.207.52.1;
  option domain-name-servers 10.207.52.1; # I control that, check https://try.popho.be/securing-home3.html
  # NB: use a forward-zone for orange.fr or use Orange's DNS - they resolve some
  # domains differently! See below.
  option ntp-servers 10.207.51.1;
  range 10.207.52.100 10.207.52.200;

  host decodeur-tv {
    hardware ethernet 4x:xx:xx:xx:xx:xx;
    # https://forums.framboise314.fr/viewtopic.php?f=57&t=5960
    option option-125 00:00:0D:E9:24:04:06:3x:xx:xx:xx:xx:xx:05:0F:4x:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:06:09:4C:69:76:65:62:6F:78:20:34;
#   option option-125 00:00:0D:E9:28:04:06:3x:xx:xx:xx:xx:xx:05:0F:4x:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:06:0d:4C:69:76:65:62:6F:78:20:46:69:62:72:65;
    #                 ............^^.......^^^^^^^^^^^^^^^^^.......^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^....^^..L..i..v..e..b..o..x.. ..F..i..b..r..e
    #                             `length  `hex(first 3 bytes of Livebox MAC)  `hex(serial Livebox)                `length of next item
    option domain-name "orange.fr";
    option domain-search "ORL.access.orange-multimedia.net";
  }
}

§ IGMPProxy

This is for live TV, using IGMP on the Orange network. The router has to have vlan840 interface on the uplink, with a random IP address on it.

Largely inspired by this OpenBSD Journal article.

chroot /var/empty
user _igmpproxy
quickleave

phyint vlan840 upstream  ratelimit 0
  altnet 0.0.0.0/0

phyint vlan52 downstream  ratelimit 0

§ pf.conf(5)

tv_in_if = "vlan52"
tv_if    = "vlan840"

...

# TV stuff is prio 5? https://lafibre.info/remplacer-livebox/tuto-remplacer-la-livebox-par-un-routeur-dd-wrt-internet-tv/
match out log on $tv_if proto igmp set (prio 5, tos 0xc0)
match out log on egress inet  from ($tv_in_if:network)  to ! (self:network) nat-to (egress)

pass  in quick log on $tv_in_if to 224.0.0.0/4 allow-opts # decoder -> router
pass out quick log on $tv_if    to 224.0.0.0/4 allow-opts # router  -> decoder
pass  in quick log on $tv_if    inet proto udp from any to 224.0.0.0/4 port { 8200 8202 } # we avoid 'any' and can't use '($tv_in_if:network)'
pass     quick log on { $tv_if $tv_in_if } proto igmp allow-opts
# no idea what 7443 and 9443 ports are for, but they seem to be necessary
pass  in quick log on $tv_in_if proto { tcp udp } to ! (self:network) port { ntp https http 7443 9443 } flags any

The TV decoder can now access live TV!

§ gaslighting DNS

We already covered that part in a previous article. M6+ is using known advertising servers, so needs no further config.

After quickly trying TF1’s replay service, it looks like some ads make it through, but suspicious domains can be listed:

tcpdump -nei vlan52 -s 65535 port domain

Orange is doing some strange things with DNS (it’s always DNS) so by telling the TV decoder to use our gaslighting DNS, we will run into issues (try resolving authentification.stb.orion.itv.orange.fr.). A specific forward-zone can be configured:

forward-zone:
    name: orange.fr
    # DNS servers from the DHCP leases
    forward-addr: 80.xx.xx.xx
    forward-addr: 81.xx.xx.xx

TODO: restrict that forward-zone to only 10.207.52.0/24.

For what it’s worth, here is the list of allow- and block-listed domains for my LG C1:

# TV
## LG OS
lgtvcommon.com
lge.com
lgeapi.com
# Only fr. needed for primevideo to work (refuses to work otherwise)
fr.lgtvsdp.com
fr.security.lgtvsdp.com
apple.com
cdn-apple.com
# Apple CDN and related stuff
mzstatic.com
lgtviot.com
d184dfn36gombl.cloudfront.net
lgappstv.com
go.microsoft.com
NevoAI-IOTHub-43-Prod.azure-devices.net

## primevideo
# same site as Netflix
aiv-cdn.net
aiv-delivery.net
amazonvideo.com
atv-ext-eu.amazon.com
atv-ext-fe.amazon.com
atv-ext.amazon.com
atv-ps-eu.amazon.co.uk
atv-ps-eu.amazon.com
atv-ps-fe.amazon.co.jp
atv-ps-fe.amazon.com
atv-ps.amazon.com
primevideo.com
pv-cdn.net
video.a2z.com
media-amazon.com
api.amazon.com
api.amazon.co.uk
fls-eu.amazon.com
s3.amazonaws.com

# Disney+
bamgrid.com
disneyplus.com
disney-plus.net
dssott.com
www.googleapis.com

# Not sure
ssl-images-amazon.com
securetime.playready.microsoft.com
akamaihd.net
ia.media-imdb.com

# TLS
ocsp.digicert.com
crl3.digicert.com
# From https://raw.githubusercontent.com/Perflyst/PiHoleBlocklist/master/SmartTV.txt
aic-ngfts.lge.com
ngfts.lge.com
cdpbeacon.lgtvcommon.com
rdl.lgtvcommon.com
sports.lgtviot.com

lgtvonline.lge.com
# https://old.reddit.com/r/LGOLED/comments/qt392k/remove_recommended_and_trending_videos_on_webos/
homeprv.lgtvcommon.com
nudge.lgtvcommon.com
recommend.lgtvcommon.com
service.lgtvcommon.com
#!/bin/sh
# transform a list into a redirect file for unbound(8)

: "${tag?"\$tag env not set"}"

awk -v "tag=$tag" \
 'BEGIN { if( tag == "" ) { tag = "iot_allowlist" } }
  /^#/ { print $0 }
  /^[^ #]+/ { printf("local-zone: %s redirect\nlocal-zone-tag: %s \"%s\"\n", $1, $1, tag ) }'
< /var/unbound/etc/iot_blocklist.raw | tag=iot_blocklist /usr/local/bin/list_to_unbound > /var/unbound/etc/iot_blocklist.conf
[ SNIP ]

        # IoT
        access-control-tag: 10.207.51.0/24 "bad malware iot_allowlist iot_blocklist"
        access-control-tag-action: 10.207.51.0/24 "iot_allowlist" transparent
        access-control-tag-data: 10.207.51.0/24 "bad"            "A 127.0.51.1"
        access-control-tag-data: 10.207.51.0/24 "bad"            "TXT bad"
        access-control-tag-data: 10.207.51.0/24 "iot_blocklist"  "A 127.0.51.2"
        access-control-tag-data: 10.207.51.0/24 "iot_blocklist"  "TXT iot_blocklist"
        access-control-tag-data: 10.207.51.0/24 "malware"        "A 127.0.51.3"
        access-control-tag-data: 10.207.51.0/24 "malware"        "TXT malware"
        # define the blocklist
        local-zone-tag: . "iot_blocklist"
        local-zone: . redirect
        # the lists are stored elsewhere
        include: /var/unbound/etc/iot_allowlist.conf
        include: /var/unbound/etc/iot_blocklist.conf

§ Amazon Prime

In 2024, Amazon started injecting ads into its Prime Video service for which I already pay 70€/year. I’m not paying 50% more for the privilege of not watching ads.

This time, I’m using the Prime app on my TV itself (4k, HDR), not on the decoder. The TV is inside its VLAN (number 51) with my robot vacuum and my doorbell, with only a DNS allowlist.

Check the usual suspects:

tcpdump -n -r /var/log/pflog port http
Jun 20 16:15:24.577397 2a01:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx.54339 > 2a00:1450:4007:81a::2003.80: S 3804016915:3804016915(0) win 64800 <[|tcp]> [flowlabel 0xa4146]
Jun 20 16:25:13.652786 2a01:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx.54398 > 2606:4700::6812:154c.80: S 3071556733:3071556733(0) win 64800 <[|tcp]> [flowlabel 0xce112]

Ah, that’s helpful. Thank you Amazon!

A short tcpdump(8) capture later, we have some suspects:

GET /interstitial/3/8629688/video_init.mp4?amznDtid=xxxxxxxxxxxx
GET /interstitial/3/8629688/audio_init.mp4?amznDtid=xxxxxxxxxxxx
GET /interstitial/3/8629688/video_39.mp4?amznDtid=xxxxxxxxxxxx
...
GET /ddf3/76ed/6401/4699-8471-3e4963bc91b1/563bf221-69e7-4ca2-bb11-299505c8bac8_video_10.mp4?amznDtid=xxxxxxxx
GET /ddf3/76ed/6401/4699-8471-3e4963bc91b1/563bf221-69e7-4ca2-bb11-299505c8bac8_audio_143.mp4?amznDtid=xxxxxxxx
GET /ddf3/76ed/6401/4699-8471-3e4963bc91b1/563bf221-69e7-4ca2-bb11-299505c8bac8_audio_143.mp4?amznDtid=xxxxxxxx
GET /ddf3/76ed/6401/4699-8471-3e4963bc91b1/563bf221-69e7-4ca2-bb11-299505c8bac8_audio_143.mp4?amznDtid=xxxxxxxx
GET /ddf3/76ed/6401/4699-8471-3e4963bc91b1/563bf221-69e7-4ca2-bb11-299505c8bac8_audio_143.mp4?amznDtid=xxxxxxxx
GET /ddf3/76ed/6401/4699-8471-3e4963bc91b1/563bf221-69e7-4ca2-bb11-299505c8bac8_video_10.mp4?amznDtid=xxxxxxxx
GET /ddf3/76ed/6401/4699-8471-3e4963bc91b1/563bf221-69e7-4ca2-bb11-299505c8bac8_audio_143.mp4?amznDtid=xxxxxxxx
GET /ddf3/76ed/6401/4699-8471-3e4963bc91b1/563bf221-69e7-4ca2-bb11-299505c8bac8_audio_143.mp4?amznDtid=xxxxxxxx
GET /ddf3/76ed/6401/4699-8471-3e4963bc91b1/563bf221-69e7-4ca2-bb11-299505c8bac8_video_10.mp4?amznDtid=xxxxxxxx
GET /ddf3/76ed/6401/4699-8471-3e4963bc91b1/563bf221-69e7-4ca2-bb11-299505c8bac8_audio_143.mp4?amznDtid=xxxxxxxx
GET /ddf3/76ed/6401/4699-8471-3e4963bc91b1/563bf221-69e7-4ca2-bb11-299505c8bac8_audio_143.mp4?amznDtid=xxxxxxxx
...

§ relayd(8)

From the manpage:

Layer 7 relaying happens at the application level and is handled by relayd itself. Various application level filtering and protocol-specific load-balancing options are available for relays.

Looking through examples (/etc/examples/relayd.conf), I found exactly what I needed! It only needed to be tweaked for the listen on directive: because Orange doesn’t guarantee addresses are stable, I can’t use the IPs of the vlan51 interface. Instead, I had to listen on lo0.

#
# Relay and protocol for a transparent HTTP proxy
#
http protocol httpfilter {
        # Return HTTP/HTML error pages to the client
        return error

        # Block disallowed URLs
        match request label "URL filtered!"
        block request quick url "pop-fr-cf.dash.pv-cdn.net/interstitial/" value "*"
}

relay httpproxy {
        # Listen on localhost, accept diverted connections from pf(4)
        listen on 127.0.0.1 port 8080
        listen on ::1 port 8080
        protocol httpfilter

        # Forward to the original target host
        forward to destination
}

§ pf.conf(5)

We use divert-to as suggested in relayd.conf(5).

pass in quick log on $iot_if inet  proto tcp to ! (self:network) port www divert-to 127.0.0.1 port 8080
pass in quick log on $iot_if inet6 proto tcp to ! (self:network) port www divert-to ::1       port 8080

Syntax nitpick: not possible to implicitly use both IPv4 and IPv6 with e.g.

pass in quick log on $iot_if proto tcp to ! (self:network) port www divert-to lo0 port 8080
# does NOT expand to cover both 127.0.0.1 and ::1 ----------------------------^^^

And… that’s it.