TP-Link Kasa KC120 – Streaming without Kasa

The main problems I have with IoT devices are:

  • They might send data home without me knowing about it
    • But I can monitor their traffic pattern and if they send home way more data than expected, I could disconnect them
  • They might be vulnerable to exploits
    • But I can put them on a separate VLAN at home so they don’t see other devices unless I allow it (via firewall rules)
    • I can sometimes update firmware (definitely a problem after few years)
  • They stop to work when the company turns off their servers
    • I am able to use them without Internet connectivity

Most Kasa products I own (power switches) are supported by various projects like Home Assistant or python-kasa, so turning on my Kasa power switch on my own is a simple task. Same for my LIFX light bulbs there’s even an official API.

The TP-Link KC120 camera however does not have any supported local API and contrary to my expectation, it does not support a local stream mode via a web browser interface. I can watch a live (and local) video stream via the Kasa application on the phone, but that functionality is at the mercy of TP-Link. I don’t like that.

Following are the steps to have local streaming (resp. recording) for the KC120. And with that it’s possible to do whatever I’d like to do with the stream: publishing on the Internet, processing via OpenCV, local archiving etc.

python-kasa

python-kasa does not support the camera, so you won’t see it during a normal discovery:

❯ kasa
No host name given, trying discovery..
Discovering devices on 255.255.255.255 for 3 seconds
== Plug Three - HS105(JP) ==
        Host: 192.168.21.180
        Device state: OFF

        == Generic information ==
        Time:         2022-05-03 11:37:55 (tz: {'index': 90, 'err_code': 0}
        Hardware:     2.1
        Software:     1.0.3 Build 210506 Rel.161924
        MAC (rssi):   10:27:F5:XX:XX:XX (-62)
        Location:     {'latitude': XX.0, 'longitude': XX.0}

        == Device specific information ==
        LED state: True
        On since: None

        == Modules ==
        + <Module Schedule (schedule) for 192.168.21.130>
        + <Module Usage (schedule) for 192.168.21.130>
        + <Module Antitheft (anti_theft) for 192.168.21.130>
        + <Module Time (time) for 192.168.21.130>
        + <Module Cloud (cnCloud) for 192.168.21.130>

== Plug One - HS105(JP) ==
        Host: 192.168.21.182
        Device state: OFF

        == Generic information ==
        Time:         2022-05-03 11:37:55 (tz: {'index': 90, 'err_code': 0}
        Hardware:     1.0
        Software:     1.5.8 Build 191125 Rel.135255
        MAC (rssi):   B0:BE:76:XX:XX:XX (-54)
        Location:     {'latitude': XX.0, 'longitude': XX.0}

        == Device specific information ==
        LED state: True
        On since: None

        == Modules ==
        + <Module Schedule (schedule) for 192.168.21.182>
        + <Module Usage (schedule) for 192.168.21.182>
        + <Module Antitheft (anti_theft) for 192.168.21.182>
        + <Module Time (time) for 192.168.21.182>
        + <Module Cloud (cnCloud) for 192.168.21.182>

But the camera shows up with an additional -d switch, although it’s being ignored since the tool does not know how to handle it:

❯ kasa -d
No host name given, trying discovery..
Discovering devices on 255.255.255.255 for 3 seconds
DEBUG:kasa.discover:[DISCOVERY] ('255.255.255.255', 9999) >> {'system': {'get_sysinfo': None}}
DEBUG:kasa.discover:Waiting 3 seconds for responses...
[...]
DEBUG:kasa.discover:Unable to find device type from {'system': {'get_sysinfo': {'err_code': 0, 'system': {'sw_ver': '2.3.6 Build 20XXXXXX rel.XXXXX', 'hw_ver': '1.0', 'model': 'KC120(EU)', 'hwId': 'CBXXXXD5XXXXDEEFA98A18XXXXXX65CD', 'oemId': 'A2XXXX60XXXX108AD36597XXXXXX572D', 'deviceId': '80XXXX88XXXX76XXXX88XXXXX3AXXXXXXXXXXXB6', 'dev_name': 'Kasa Cam', 'c_opt': [0, 1], 'f_list': [], 'a_type': 2, 'type': 'IOT.IPCAMERA', 'alias': 'Camera', 'mic_mac': 'D80D17XXXXXX', 'mac': 'D8:0D:17:XX:XX:XX', 'longitude': XX, 'latitude': XX, 'rssi': -38, 'system_time': 1651545748, 'led_status': 'on', 'updating': False, 'status': 'configured', 'resolution': '720P', 'camera_switch': 'on', 'bind_status': True, 'last_activity_timestamp': 1651545210}}}}: Unable to find the device type field!
[...]

Important fields here are the deviceID and via the MAC address, you can find out what IP address the camera has (if you use DHCP). In my case 192.168.21.187 is the camera’s IP address.

nmap

nmap shows only port 9999 open which is the known TP-Link debug port. But there’s more ports:

❯ sudo nmap -p- 192.168.21.187
Starting Nmap 7.80 ( https://nmap.org ) at 2022-05-03 11:51 JST
Nmap scan report for kc120.lan (192.168.21.187)
Host is up (0.012s latency).
Not shown: 65531 closed ports
PORT      STATE SERVICE
9999/tcp  open  abyss
10443/tcp open  unknown
18443/tcp open  unknown
19443/tcp open  unknown
MAC Address: D8:0D:17:XX:XX:XX (Tp-link Technologies)

Nmap done: 1 IP address (1 host up) scanned in 9.28 seconds

And with that port information I found this article: https://medium.com/@hu3vjeen/reverse-engineering-tp-link-kc100-bac4641bf1cd. It’s about a slightly different camera model, but since the ports patch, maybe more does.

I followed it, however I could not get the authentication working: the Kasa account password as per article did not work. Time to do the ARP spoofing to see what the Android app uses to authenticate! Geistless did a great job explaining the steps he took.

My overall plan:

  1. Redirect the traffic from the Kasa app on the phone to my Linux machine (via arpspoof)
  2. Redirect the incoming HTTPS traffic to my HTTPS server (via iptables)
  3. Print the URL and headers for incoming HTTPS traffic which arrives at my HTTPS server

arpspoof

The dsniff package contains arpspoof:

❯ sudo apt install dsniff
[...]
❯ sudo setcap CAP_NET_RAW+ep /usr/sbin/arpspoof

My HTTPS Server

While the original author had a https server as part of his Rust learning, I created a NodeJS version. But first we’ll need keys. Self-signed is fine:

❯ openssl genrsa -out key.pem
❯ openssl req -new -key key.pem -out csr.pem
❯ openssl x509 -req -days 999 -in csr.pem -signkey key.pem -out cert.pem
❯ rm csr.pem

Now the simple HTTPS server listening on port 8080:

const https = require('https');
const fs = require('fs');

const options = {
  key: fs.readFileSync('key.pem'),
  cert: fs.readFileSync('cert.pem')
};

https.createServer(options, function (req, res) {
  console.log(req.url);
  console.log(req.headers);
  res.writeHead(200);
  res.end("");
}).listen(8080);

Some IP traffic routing rules to redirect all incoming TCP traffic on enp1s0 for ports 10443, 18443 and 19443 to port 8080:

❯ sudo iptables -t nat -A PREROUTING -i enp1s0 -p tcp --dport 10443 -j REDIRECT --to-port 8080
❯ sudo iptables -t nat -A PREROUTING -i enp1s0 -p tcp --dport 18443 -j REDIRECT --to-port 8080
❯ sudo iptables -t nat -A PREROUTING -i enp1s0 -p tcp --dport 19443 -j REDIRECT --to-port 8080
❯ sudo sysctl net.ipv4.ip_forward=1

Now run the https server and watch it display the URL and the headers for an incoming request on port 19443:

❯ node ./https.js

and to test, on another machine I ran:

$ curl -k -u admin:abc 'https://t621.lan:19443/test?a=3&b=5'

and this is the output of my https server:

/test?a=3&b=5
{
  host: 't621.lan:19443',
  authorization: 'Basic YWRtaW46YWJj',
  'user-agent': 'curl/7.68.0',
  accept: '*/*'
}

The basic authentication is base64 encoded. To decode:

❯ echo YWRtaW46YWJj | base64 -d
admin:abc

So that works. Now putting it all together.

  • Start the Kasa app on the phone. Make sure the KC120 is enabled and can display a live video stream. Stop the stream.
  • Have the iptables redirect rules in place. And IP forwarding in the kernel.
  • Start the HTTPS server.
  • Run arpspoof. 192.168.21.55 is the phone’s IP which runs the Kasa application. 192.168.21.187 is the IP of the KC120.
❯ arpspoof -i enp1s0 -t 192.168.21.55 192.168.21.187
7c:d3:a:xx:xx:xx 38:78:62:xx:xx:xx 0806 42: arp reply 192.168.21.187 is-at 7c:d3:a:xx:xx:xx
  • On the mobile app, try to connect to the video stream of the KC120 again
  • You should now see some output of the HTTPS server:
/https/stream/mixed?video=H264&audio=G711
{
  authorization: 'Basic aXXXXXXXXXXXXXXXXM=',
  connection: 'keep-alive',
  'user-agent': 'Dalvik/2.1.0 (Linux; U; Android 10; H8296 Build/52.1.A.3.49)',
  host: '192.168.21.187:19443',
  'accept-encoding': 'gzip'
}

And then I finally had the authentication string the camera wanted!

❯ echo 'aXXXXXXXXXXXXXXXXM=' | base64 -d
MY_KASA_ACCOUNT:THE_CAMERA_PASSWORD

Turns out that the password to use was not the Kasa password: it’s a longish string of hex digits. That might be a KC120 specialty or it might depend on the firmware version. I cannot say since I have no KC100, but whatever the password is, it’s possible to find out relatively easily using above approach.

The Result: Local Streaming!

I can connect to the video stream! And with very little CPU usage too.

❯ curl -k -u 'MY_KASA_ACCOUNT:THE_CAMERA_PASSWORD' \
--ignore-content-length \
"https://192.168.21.187:19443/https/stream/mixed?video=h264&audio=g711&resolution=hd&deviceId=80XXXX88XXXX76XXXX88XXXXX3AXXXXXXXXXXXB6" \
--output - | ffmpeg -hide_banner -y -i - -vcodec copy kc120stream.mp4

To change resolution, change it in the Kasa app. 1920×1080 (1.4Mb/s), 1280×720 (850kbit/s) and 640×360 (350kbit/s) are possible.

TODO

  • There is no audio coming from the camera. Audio works on the Kasa app.
  • It would also be nice to understand how to change the configuration of the camera (e.g. change resolution), but it’s ok to set them once via the Kasa app.
  • What options do the parameter video, audio and resolution support?

Time Lapse Videos

I got a TP-Link KC120. Looks good. Good lens. Very nice magnetic stand. But the firmware…I should have checked that there is an API to get a single frame or a video stream out of it. Because there is no such thing on that camera which means it has to be used via its TP-Link software which is not to my liking.

Since there seems to be no alternative firmware and TP-Link seems to not develop this camera further, I am looking for alternatives. There’s good info at https://www.home-assistant.io/integrations/#camera for exactly that. The Android IP WebCam caught my eye as I use an Android app for adding a camera to OBS Studio via DroidCam OBS which uses the NDI protocol to convert an Android phone into a NDI camera. Android IP WebCam is the same idea (and yes, I probably could have used the DroidCam OBS software too).

So I tried it and it’s everything I ever wanted from a security web cam: I can do singe shoot pictures (great for time lapse). It can record continuously. Detect motion, or things happening on the screen. Here’s the screenshot of the configuration screen:

Android IP WebCam Control Screen

The only drawback is that the lens of the phone is not wide-angle. Next Android phone will have a wider angle lens.

But beside this, finally I can do time laps videos. Here the recording part. Note the attempt to make sure to get a frame every X seconds. WiFi being as unreliable as it is, wget sometimes hangs for 15min (its normal timeout) which game me initially random 15min gaps.

#!/bin/bash
set -euo pipefail

# Record single pictures
# Maybe add time stamp to each

prefix="s"

export cnt=0
export pic_every_sec=30
export timeout_in_sec=25

rm -f shot.jpg

while true ; do
  sleep $pic_every_sec &
  ( date +%T.%6N
  t=$(printf "%05d" $cnt)
  wget -q --timeout=$timeout_in_sec --user=USERNAME--password="PASSWORD" http://192.168.21.115:8080/shot.jpg
  if [[ $? -eq 0 ]] ; then
    exiv2 -M"set Exif.Photo.DateTimeOriginal $(date +'%Y:%m:%d %H:%M:%S')" shot.jpg
    mv shot.jpg ${prefix}-$t.jpg
  else
    rm -f shot.jpg
  fi
  )
  let cnt=cnt+1
  wait
done

And here the making of a movie:

#!/bin/bash
set -euo pipefail

prefix="s"

# Add timestamp
for i in ${prefix}-* ; do
  timestamp=$(exiv2 -g Exif.Photo.DateTimeOriginal -Pv $i | sed 's/:/-/1;s/:/-/1')
  echo $timestamp >&2
  echo -n $timestamp >/tmp/timestamp.txt
  ffmpeg -v quiet -i $i -vf 'drawtext=textfile=/tmp/timestamp.txt:x=(w-tw)-10:y=h-(2*lh):fontcolor=white:box=0: boxcolor=0x00000000@1:borderw=4:fontsize=40' -f mjpeg -
done | ffmpeg -v quiet -r:v 30 -i - -codec:v libx265 -threads 2 -preset fast -r:v 30 -x265-params crf=28:pools=2 -f mp4 -an the_movie.mp4

I was worried that the phone gets hot, but that turned out to be a total non-issue.

OBS and Fun With WebEx

At work we use WebEx for video conferencing. Not the worst choice. I like video conferencing as I think it makes meetings better and more fun. Also nice to see people once in a while when working from home. Then on March 15 this came up:

https://dilbert.com/strip/2021-03-15

This Dilbert strip immediately made me think: I want that too! But how?

Our WebEx is limited by corporate policies, so no fancy backgrounds and certainly no closing credit plugins if they even exist. There had to be a way: it’s a technical problem, so there must be a technical solution.

Searching a bit around made me find OBS which is normally used to stream to Twitch/YouTube/etc. or to record things like lesson videos. And it has a “Virtual Camera” feature which does exactly when you think it does: it presents its output as a new video camera device which other programs can use as their video input.

And it works! Except in WebEx. Turns out WebEx does not like this. However WebEx does not mind NDI stream as an input and OBS can output such a stream via the NDI Tools.

So all I need now is:

  1. Have OBS set up with NDI Tools
  2. Create 2 scenes in OBS: my webcam’s picture and the same plus scrolling text (AKA the closing credits)
  3. Enable NDI Output (in the Tools menu of OBS)
  4. In WebEx application, choose the NDI camera as video source
  5. When you want the closing credits to show, switch the scene

There’s of course a lot more potential fun inside OBS:

Side Effects

As an unexpected side effect while learning about OBS, I fixed my light during video conferences (Key/Fill/Back lights), made me stand out from the background more, learned how boring cameras are when they statically point at you, made the sound better via some audio filters, and I looked into the world of Shaders, which are super interesting but I think that’s too deep into that rabbit hole…

Video Editing on Linux – OpenShot

Video editing is fun, but I suck at it. So I keep it simple. Long time I used Kino but it’s no longer developed. But it had 3 main things:

  • It ran on Linux
  • It was simple
  • It worked with my DV camera (FireWire AKA IEEE-1394, remember that?)

Since I had a small video to “edit” (mainly add a title, cut off some seconds from the start and end), I looked and found a Kino replacement: OpenShot. It’s simple enough to almost immediately use without a steep learning curve. I like it.