Rafael Rivera

Pairing a stubborn Reolink RLC-511W camera with Windows 10

Windows 10 Camera Settings window

Microsoft added a new Camera Settings page to Windows 10 build 21354 a few weeks ago. But I wasn't able to get it working with my Reolink RLC-511W camera. I filed a bug and was about to move on but curiosity set in.

Diagnosis

To take a peek at the communication between Windows 10 and the camera, I installed Wireshark and began capturing traffic. After clicking the "Add a network camera" button in Windows, Web Services on Devices Discovery (WS-Discovery) Probe messages were immediately broadcast across the local network. The messages looked like this:

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope [...]>
<soap:Header>
<wsa:To>urn:schemas-xmlsoap-org:ws:2005:04:discovery</wsa:To>
<wsa:Action>
http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe
</wsa:Action>
<wsa:MessageID>
urn:uuid:7c206a7a-baba-4e7e-b8c2-1dec6fd798f4
</wsa:MessageID>
</soap:Header>
<soap:Body>
<wsd:Probe>
<wsd:Types>dn:NetworkVideoTransmitter</wsd:Types>
</wsd:Probe>
</soap:Body>
</soap:Envelope>

The camera correctly recognized that someone was looking for an ONVIF network video transmitter and replied with a ProbeMatches message. That message looked like this:

<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope [...]>
<SOAP-ENV:Header>
<wsa:MessageID>
uuid:2419d68a-2dd2-21b2-a205-ec:71:db:8c:14:92
</wsa:MessageID>
<wsa:RelatesTo>
urn:uuid:516be46d-86ae-4c0c-b852-a93954ffade0
</wsa:RelatesTo>
<wsa:To SOAP-ENV:mustUnderstand="1">
http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous
</wsa:To>
<wsa:Action>
http://schemas.xmlsoap.org/ws/2005/04/discovery/ProbeMatches
</wsa:Action>
</SOAP-ENV:Header>
<SOAP-ENV:Body>
<wsdd:ProbeMatches>
<wsdd:ProbeMatch>
<wsa:EndpointReference>
<wsa:Address>
urn:uuid:2419d68a-2dd2-21b2-a205-ec:71:db:8c:14:92
</wsa:Address>
<wsa:ReferenceProperties></wsa:ReferenceProperties>
<wsa:ReferenceParameters></wsa:ReferenceParameters>
<wsa:PortType>ttl</wsa:PortType>
</wsa:EndpointReference>
<wsdd:Types>tdn:NetworkVideoTransmitter</wsdd:Types>
<wsdd:Scopes>
onvif://www.onvif.org/type/video_encoder
onvif://www.onvif.org/location/country/china
onvif://www.onvif.org/type/network_video_transmitter
onvif://www.onvif.org/hardware/IPC-122
onvif://www.onvif.org/Profile/Streaming
onvif://www.onvif.org/name/IPC-BO
</wsdd:Scopes>
<wsdd:XAddrs>
http://192.168.1.191:8000/onvif/device_service
</wsdd:XAddrs>
<wsdd:MetadataVersion>1</wsdd:MetadataVersion>
</wsdd:ProbeMatch>
</wsdd:ProbeMatches>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

Unfortunately, the response contained a few errors. I've highlighted those in yellow. Let's take a closer look at them, starting from the top:

  • MessageID: The camera provided 2419d68a-2dd2-21b2-a205-ec:71:db:8c:14:92, which is not a valid UUID.

    In subsequent tests, the camera also reused this value in repeated conversations. That's not good. Windows 10 will ignore messages with reused MessageID values, following good security practice to prevent replay attacks.

  • Address: The camera provided another invalid UUID.

  • PortType: The camera provided an unqualified value that doesn't map to anything that makes sense.

Windows 10 Camera Settings window showing an unknown camera device named IPC-BO

After fixing these issues (described later), I rescanned for cameras. The Camera Settings page found the camera, but clicking "Add device" just threw an "Couldn't add the device. Try again." error.

So I continued my review of the message traffic.

The earlier ProbeResponse message contained a transport address (XAddr), meaning Windows 10 didn't have to search the network for the camera by name (with Resolve messages). It immediately jumped to calling methods on the camera via its transport address (http://192.168.1.191:8000/onvif/device_service) using SOAP over HTTP.

Windows 10 called the camera's ONVIF Device Service method GetServiceCapabilities to get an idea of what it can and can't do:

POST /onvif/device_service HTTP/1.1
Cache-Control: no-cache
Connection: Keep-Alive
Pragma: no-cache
[...]
User-Agent: MS-WebServices/1.0
Content-Length: 164
Host: 192.168.1.191:8000

<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">
<s:Body>
<GetServiceCapabilities xmlns="http://www.onvif.org/ver10/device/wsdl" />
</s:Body>
</s:Envelope>

The camera responded with:

HTTP/1.1 200 OK
Server: gSOAP/2.8
[...]
Content-Length: 3694
Connection: close

<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope [...]>
<SOAP-ENV:Body>
<tds:GetServiceCapabilitiesResponse>
<tds:Capabilities>
<tds:Network
NTP="1" HostnameFromDHCP="false" Dot11Configuration="false"
DynDNS="false" IPVersion6="false" ZeroConfiguration="false"
IPFilter="false">

</tds:Network>
<tds:Security RELToken="false" HttpDigest="true" UsernameToken="true"
KerberosToken="false" SAMLToken="false" X.509Token="false"
RemoteUserHandling="false" Dot1X="false" AccessPolicyConfig="true"
OnboardKeyGeneration="false" TLS1.2="false" TLS1.1="false"
TLS1.0="false">

</tds:Security>
<tds:System HttpSupportInformation="true" HttpSystemLogging="false"
HttpSystemBackup="false" HttpFirmwareUpgrade="false"
FirmwareUpgrade="false" SystemLogging="false" SystemBackup="false"
RemoteDiscovery="true" DiscoveryBye="true" DiscoveryResolve="true">

</tds:System>
</tds:Capabilities>
</tds:GetServiceCapabilitiesResponse>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

Windows then requested the camera date and time, for time synchronization purposes. (Authentication requires reasonably synchronized time.)

POST /onvif/device_service HTTP/1.1
Cache-Control: no-cache
Connection: Keep-Alive
Pragma: no-cache
[...]
User-Agent: MS-WebServices/1.0
Content-Length: 162
Host: 192.168.1.191:8000

<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">
<s:Body>
<GetDeviceInformation xmlns="http://www.onvif.org/ver10/device/wsdl" />
</s:Body>
</s:Envelope>

The camera responded with:

HTTP/1.1 200 OK
Server: gSOAP/2.8
[...]
Content-Length: 3611
Connection: close

<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope [...]>
<SOAP-ENV:Body>
<tds:GetSystemDateAndTimeResponse>
<tds:SystemDateAndTime>
<tt:DateTimeType>Manual</tt:DateTimeType>
<tt:DaylightSavings>false</tt:DaylightSavings>
<tt:TimeZone>
<tt:TZ>
GMT+8:00:00DST+7:00:00,M3.2.0/01:59:00,M11.1.0/01:59:00
</tt:TZ>
</tt:TimeZone>
<tt:UTCDateTime>
<tt:Time>
<tt:Hour>6</tt:Hour>
<tt:Minute>16</tt:Minute>
<tt:Second>32</tt:Second>
</tt:Time>
<tt:Date>
<tt:Year>2021</tt:Year>
<tt:Month>4</tt:Month>
<tt:Day>23</tt:Day>
</tt:Date>
</tt:UTCDateTime>
<tt:LocalDateTime>
<tt:Time>
<tt:Hour>23</tt:Hour>
<tt:Minute>16</tt:Minute>
<tt:Second>32</tt:Second>
</tt:Time>
<tt:Date>
<tt:Year>2021</tt:Year>
<tt:Month>4</tt:Month>
<tt:Day>22</tt:Day>
</tt:Date>
</tt:LocalDateTime>
<tt:Extension></tt:Extension>
</tds:SystemDateAndTime>
</tds:GetSystemDateAndTimeResponse>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

Next, Windows 10 called the camera's ONVIF Device Service method GetDeviceInformation:

POST /onvif/device_service HTTP/1.1
Cache-Control: no-cache
Connection: Keep-Alive
Pragma: no-cache
[...]
User-Agent: MS-WebServices/1.0
Content-Length: 162
Host: 192.168.1.191:8000

<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">
<s:Body>
<GetDeviceInformation xmlns="http://www.onvif.org/ver10/device/wsdl" />
</s:Body>
</s:Envelope>

The camera responded with:

HTTP/1.1 400 Bad Request
Server: gSOAP/2.8
Content-Type: application/soap+xml; charset=utf-8
Content-Length: 3232
Connection: close

<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope
xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope" [...]>

<SOAP-ENV:Body>
<SOAP-ENV:Fault SOAP-ENV:encodingStyle="[...]">
<SOAP-ENV:Code>
<SOAP-ENV:Value>SOAP-ENV:Sender</SOAP-ENV:Value>
<SOAP-ENV:Subcode>
<SOAP-ENV:Value>ter:NotAuthorized</SOAP-ENV:Value>
</SOAP-ENV:Subcode>
</SOAP-ENV:Code>
<SOAP-ENV:Reason>
<SOAP-ENV:Text xml:lang="en">Sender not Authorized</SOAP-ENV:Text>
</SOAP-ENV:Reason>
</SOAP-ENV:Fault>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

Oh dear. As before, I've highlighted problem areas in yellow. Starting from the top:

  • HTTP/1.1 400 response code: Per the ONVIF Core Specification, the camera—claiming to support both token and digest authentication—must respond with a 401 error:

    If server supports both digest authentication [...] and the user name token profile [...] the following behavior shall be adapted: [...] If a client does not supply authentication credentials along with a web service request, the server shall assume that the client intends to use digest authentication [RFC 2617], if required. Hence, if a client does not provide authentication credentials when requesting a service that requires authentication, it will receive an HTTP 401 error according to [RFC 2617]. Note that this behaviour on the server’s side differs from the case of supporting only username token profile, which requires for this case an HTTP 400 error on the HTTP level and a SOAP:Fault env:Sender ter:NotAuthorized error on the WS level.

  • encodingStyle: This attribute is not supported on the Fault element. The SOAP 1.2 specification makes this explicitly clear:

    The encodingStyle attribute information item MAY appear on the following:

    [...] A child element information item of the SOAP Body element information item (see 5.3.1 SOAP Body child Element) if that child is not a SOAP Fault element information item (see 5.4 SOAP Fault). [...]

Windows 10 Camera Settings window showing camera device configured successfully

Windows 10 Camera Settings window showing camera device configured successfully

After I patched the camera's GetServiceCapabilities response, dropping HTTP digest authentication support and the invalid attribute, Windows 10 authenticated and paired with the camera successfully.

Quick Fix Engineering

The camera is already running the latest firmware available (20121804) so hoping to address the problems via an update was out.

And I wasn't particularly interested in modifying the firmware and/or rooting the camera either.

So because I already had a utility Linux PC with multiple Ethernet ports lying around in my homelab, I opted to use it as a monkey-in-the-middle, correcting the camera message traffic on-the-fly.

I created a simple network bridge on the Linux PC using bridge-nf. I then added a few iptables rules to forward camera-specified traffic into a queue (NFQUEUE) that is accessible via user-mode software.

Using Python, Scapy (a packet manipulation multi-tool), and NetFilterQueue (Python bindings for libnetfilter_queue), I was able to quickly patch up those camera errors with a simple script:

from scapy.all import IP, TCP, UDP, Raw
from netfilterqueue import NetfilterQueue
from collections import namedtuple

camera_ip = '192.168.1.191'
replacement = namedtuple('replacement', ['old', 'new'])

# The replacements are crafted to avoid changing the
# original packet length. This ensures we don't
# interfere with IP fragmentation.

replacements = [
replacement(
b'<wsa:MessageID>'
b'uuid:2419d68a-2dd2-21b2-a205-ec:71:db:8c:14:92'
b'</wsa:MessageID>',

b'<wsa:MessageID>'
b'uuid:2419d68a-2dd2-21b2-a205-ec71db8c1492'
b'</wsa:MessageID>'
),
replacement(
b'<wsa:Address>'
b'urn:uuid:2419d68a-2dd2-21b2-a205-ec:71:db:8c:14:92'
b'</wsa:Address>',

b'<wsa:Address>'
b'urn:uuid:2419d68a-2dd2-21b2-a205-ec71db8c1492'
b'</wsa:Address>'
),
replacement(
b'<wsa:PortType>ttl</wsa:PortType>',
b'<!-- -->'
),
replacement(
b'HttpDigest="true"',
b' '
),
replacement(
b'SOAP-ENV:encodingStyle="http://www.w3.org/2003/05/soap-encoding"',
b' '
)
]


def zero_checksum_and_length(packet):
# Resetting the length of the packet forces a
# recalculation of the checksum.

if(packet.haslayer(TCP)):
packet[TCP].chksum = None
packet[TCP].len = None
if(packet.haslayer(UDP)):
packet[UDP].chksum = None
packet[UDP].len = None
packet.chksum = None


def callback(nfpacket):
packet = IP(nfpacket.get_payload())
if(packet.haslayer(Raw)):
payload = packet[Raw].load

if(packet.src == camera_ip):
for replacement in replacements:
payload = payload.replace(replacement.old, replacement.new)
elif(packet.dst == camera_ip):
for replacement in replacements:
payload = payload.replace(replacement.new, replacement.old)

zero_checksum_and_length(packet)
nfpacket.set_payload(bytes(packet))
nfpacket.accept()


nfqueue = NetfilterQueue()
nfqueue.bind(1, callback)

try:
nfqueue.run()
except KeyboardInterrupt:
print('Exiting.')

nfqueue.unbind()

While the script is far from perfect (e.g. manual rotation of the MessageID is needed), it worked well enough to get the camera paired with Windows 10.