Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix ONVIF Camera Connectivity Behind NAT and Routers #15886

Draft
wants to merge 2 commits into
base: dev
Choose a base branch
from

Conversation

tibacher
Copy link

@tibacher tibacher commented Jan 8, 2025

Proposed change

This pull request addresses an issue with ONVIF cameras located behind NAT (Network Address Translation) routers in Frigate. When queried, these cameras return their private IP addresses and internal ports, making it impossible to access them externally. This issue is particularly relevant for users with cameras installed behind routers.

The solution involves rewriting the xaddrs in the ONVIF response to utilize the external IP address and port provided during the connection setup, a method already implemented in the python-onvif-zeep package.

I have tested this solution successfully in Frigate version 14.1, ensuring that it works as expected for cameras behind routers. This update will greatly improve connectivity in NAT environments and ensure that users can access their cameras from outside the local network.

Type of change

  • Dependency upgrade
  • Bugfix (non-breaking change which fixes an issue)
  • New feature
  • Breaking change (fix/feature causing existing functionality to break)
  • Code quality improvements to existing code
  • Documentation Update

Additional information

Checklist

  • The code change is tested and works locally. Also tested in realworld scenario with camera behind NAT.
  • Local tests pass. Your PR cannot be merged unless tests pass
  • There is no commented out code in this PR.
  • The code has been formatted using Ruff (ruff format frigate)

Sorry had no time sofar to formt with ruff and run the local test pass.

Copy link

netlify bot commented Jan 8, 2025

Deploy Preview for frigate-docs canceled.

Name Link
🔨 Latest commit 4e3f50f
🔍 Latest deploy log https://app.netlify.com/sites/frigate-docs/deploys/677e2a61f7c56b00083f43b6

@hawkeye217
Copy link
Collaborator

Because we recently updated the base image and python version for 0.16, we plan to upgrade to python-onvif-zeep-async at some point soon: https://github.com/openvideolibs/python-onvif-zeep-async

@NickM-27
Copy link
Collaborator

NickM-27 commented Jan 8, 2025

See #15894

@DrissiReda
Copy link

@tibacher thanks for the PR I saw your comment.

However this isn't enough to work with frigate. I had to override the transport.

This is what worked for me on the latest version on frigate. Even though I didn't get the time to make a proper PR because I don't have the time, and still need to find a way to write a test for it before I can submit it.

def contains_ip_port(s):
    # Define a regex pattern for IP:port
    ip_port_pattern = re.compile(
        r'(\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d+\b)'
    )

    # Search for the pattern in the string
    if ip_port_pattern.search(s):
        return True
    return False

def update_links(data, xaddr):
    if isinstance(data, dict):
        for key, value in data.items():
            data[key] = update_links(value, xaddr)
    elif isinstance(data, list):
        for index, item in enumerate(data):
            data[index] = update_links(item, xaddr)
    elif isinstance(data, str):
        if contains_ip_port(data):
            _, bad_addr = data.split('://')
            new_addr = str(xaddr) + '/' + bad_addr.split('/', 1)[1]
            return new_addr
    return data

class AddressOverrideTransport(Transport):
    def __init__(self, xaddr, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.override_xaddr = xaddr

    def post(self, address, message, headers):
        response = super().post(address, message, headers)
        json_resp = xmltodict.parse(response.text)
        json_resp=update_links(json_resp, self.override_xaddr)
        response._content = xmltodict.unparse(json_resp, pretty=False).encode('utf-8')
        return response

Then in class init I would have this:

    @safe_func
    def __init__(self, xaddr, user, passwd, url,
                 encrypt=True, daemon=False, zeep_client=None, no_cache=False,
                 portType=None, dt_diff=None, binding_name='', transport=None):
        if not os.path.isfile(url):
            raise ONVIFError('%s doesn`t exist!' % url)

        self.url = url
        self.xaddr = xaddr
        self.override_host = None
        self.override_port = None
        session = Session()
        if transport is None:
          self.transport = AddressOverrideTransport(self.xaddr, session=session)

@tibacher
Copy link
Author

tibacher commented Jan 21, 2025

@tibacher thanks for the PR I saw your comment.

However this isn't enough to work with frigate. I had to override the transport.

This is what worked for me on the latest version on frigate. Even though I didn't get the time to make a proper PR because I don't have the time, and still need to find a way to write a test for it before I can submit it.

Hi @DrissiReda
which version is the "latest" for you? 0.14/0.15/0.16

did you Tests my solution? If Yes which error / logs do you get. Because for me it worked after upgrading the onvif-zeep package for cameras behind a router with NAT forewarding.

Because we recently updated the base image and python version for 0.16, we plan to upgrade to python-onvif-zeep-async at some point soon: https://github.com/openvideolibs/python-onvif-zeep-async

Thanks @hawkeye217 I'll look into that but currently I have no time.

@DrissiReda
Copy link

I tested it on 0.15. Frigate shows that it fails to connect to the local address despite using the natted one in configuration. And with further debugging I realized that with your fix. If one wanted to create a new service using the python library the ips returned are all local.

from onvif import ONVIFCamera

cam = ONVIFCamera('dns.example.com', 8120, 'user', 'pass')
cam.update_xaddrs() # we have natted addresses and ports after this
event = cam.create_events_service()
print(event.xaddr) # this will however be a local address

And it's the same for other create_* functions, which is why I had to override the transport used. And I rebuilt my frigate locally using what I put earlier and it worked.

@tibacher
Copy link
Author

I tested it on 0.15.

Ok as I mentioned, my fix is for 0.14.1. Maybe that it is not working for 0.15.*

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support for ONVIF Camera Connections Behind NAT and Routers
4 participants