Skip to content

Commit

Permalink
Add support for socks connection to Git backend (#17640)
Browse files Browse the repository at this point in the history
* Add support for socks connection to Git backend

* cleanup socks detection

* add documentation for installing python_socks

* dont need lower()

* cleanup

* refactor Socks to utilities

* fix imports

* fix missing comma

* Update docs/features/synchronized-data.md

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>

* review feedback

* Update docs/features/synchronized-data.md

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>

* review changes

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
  • Loading branch information
arthanson and jeremystretch authored Oct 1, 2024
1 parent d9028f9 commit 92d8aa5
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 3 deletions.
3 changes: 3 additions & 0 deletions docs/features/synchronized-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ To enable remote data synchronization, the NetBox administrator first designates
!!! info
Data backends which connect to external sources typically require the installation of one or more supporting Python libraries. The Git backend requires the [`dulwich`](https://www.dulwich.io/) package, and the S3 backend requires the [`boto3`](https://boto3.amazonaws.com/v1/documentation/api/latest/index.html) package. These must be installed within NetBox's environment to enable these backends.

!!! info
If you are configuring Git and have `HTTP_PROXIES` configured to use the SOCKS protocol, you will also need to install the [`python_socks`](https://pypi.org/project/python-socks/) Python library.

Each type of remote source has its own configuration parameters. For instance, a git source will ask the user to specify a branch and authentication credentials. Once the source has been created, a synchronization job is run to automatically replicate remote files in the local database.

The following NetBox models can be associated with replicated data files:
Expand Down
20 changes: 17 additions & 3 deletions netbox/core/data_backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@

from django import forms
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.utils.translation import gettext as _

from netbox.data_backends import DataBackend
from netbox.utils import register_data_backend
from utilities.constants import HTTP_PROXY_SUPPORTED_SCHEMAS, HTTP_PROXY_SUPPORTED_SOCK_SCHEMAS
from utilities.socks import ProxyPoolManager
from .exceptions import SyncError

__all__ = (
Expand Down Expand Up @@ -67,11 +70,18 @@ def init_config(self):

# Initialize backend config
config = ConfigDict()
self.use_socks = False

# Apply HTTP proxy (if configured)
if settings.HTTP_PROXIES and self.url_scheme in ('http', 'https'):
if proxy := settings.HTTP_PROXIES.get(self.url_scheme):
config.set("http", "proxy", proxy)
if settings.HTTP_PROXIES:
if proxy := settings.HTTP_PROXIES.get(self.url_scheme, None):
if urlparse(proxy).scheme not in HTTP_PROXY_SUPPORTED_SCHEMAS:
raise ImproperlyConfigured(f"Unsupported Git DataSource proxy scheme: {urlparse(proxy).scheme}")

if self.url_scheme in ('http', 'https'):
config.set("http", "proxy", proxy)
if urlparse(proxy).scheme in HTTP_PROXY_SUPPORTED_SOCK_SCHEMAS:
self.use_socks = True

return config

Expand All @@ -87,6 +97,10 @@ def fetch(self):
"errstream": porcelain.NoneStream(),
}

# check if using socks for proxy - if so need to use custom pool_manager
if self.use_socks:
clone_args['pool_manager'] = ProxyPoolManager(settings.HTTP_PROXIES.get(self.url_scheme))

if self.url_scheme in ('http', 'https'):
if self.params.get('username'):
clone_args.update(
Expand Down
4 changes: 4 additions & 0 deletions netbox/utilities/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,7 @@
"td": {"align"},
"th": {"align"},
}

HTTP_PROXY_SUPPORTED_SOCK_SCHEMAS = ['socks4', 'socks4a', 'socks4h', 'socks5', 'socks5a', 'socks5h']
HTTP_PROXY_SOCK_RDNS_SCHEMAS = ['socks4h', 'socks4a', 'socks5h', 'socks5a']
HTTP_PROXY_SUPPORTED_SCHEMAS = ['http', 'https', 'socks4', 'socks4a', 'socks4h', 'socks5', 'socks5a', 'socks5h']
101 changes: 101 additions & 0 deletions netbox/utilities/socks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import logging

from urllib.parse import urlparse
from urllib3 import PoolManager, HTTPConnectionPool, HTTPSConnectionPool
from urllib3.connection import HTTPConnection, HTTPSConnection
from .constants import HTTP_PROXY_SOCK_RDNS_SCHEMAS


logger = logging.getLogger('netbox.utilities')


class ProxyHTTPConnection(HTTPConnection):
"""
A Proxy connection class that uses a SOCK proxy - used to create
a urllib3 PoolManager that routes connections via the proxy.
This is for an HTTP (not HTTPS) connection
"""
use_rdns = False

def __init__(self, *args, **kwargs):
socks_options = kwargs.pop('_socks_options')
self._proxy_url = socks_options['proxy_url']
super().__init__(*args, **kwargs)

def _new_conn(self):
try:
from python_socks.sync import Proxy
except ModuleNotFoundError as e:
logger.info("Configuring an HTTP proxy using SOCKS requires the python_socks library. Check that it has been installed.")
raise e

proxy = Proxy.from_url(self._proxy_url, rdns=self.use_rdns)
return proxy.connect(
dest_host=self.host,
dest_port=self.port,
timeout=self.timeout
)


class ProxyHTTPSConnection(ProxyHTTPConnection, HTTPSConnection):
"""
A Proxy connection class for an HTTPS (not HTTP) connection.
"""
pass


class RdnsProxyHTTPConnection(ProxyHTTPConnection):
"""
A Proxy connection class for an HTTP remote-dns connection.
I.E. socks4a, socks4h, socks5a, socks5h
"""
use_rdns = True


class RdnsProxyHTTPSConnection(ProxyHTTPSConnection):
"""
A Proxy connection class for an HTTPS remote-dns connection.
I.E. socks4a, socks4h, socks5a, socks5h
"""
use_rdns = True


class ProxyHTTPConnectionPool(HTTPConnectionPool):
ConnectionCls = ProxyHTTPConnection


class ProxyHTTPSConnectionPool(HTTPSConnectionPool):
ConnectionCls = ProxyHTTPSConnection


class RdnsProxyHTTPConnectionPool(HTTPConnectionPool):
ConnectionCls = RdnsProxyHTTPConnection


class RdnsProxyHTTPSConnectionPool(HTTPSConnectionPool):
ConnectionCls = RdnsProxyHTTPSConnection


class ProxyPoolManager(PoolManager):
def __init__(self, proxy_url, timeout=5, num_pools=10, headers=None, **connection_pool_kw):
# python_socks uses rdns param to denote remote DNS parsing and
# doesn't accept the 'h' or 'a' in the proxy URL
if use_rdns := urlparse(proxy_url).scheme in HTTP_PROXY_SOCK_RDNS_SCHEMAS:
proxy_url = proxy_url.replace('socks5h:', 'socks5:').replace('socks5a:', 'socks5:')
proxy_url = proxy_url.replace('socks4h:', 'socks4:').replace('socks4a:', 'socks4:')

connection_pool_kw['_socks_options'] = {'proxy_url': proxy_url}
connection_pool_kw['timeout'] = timeout

super().__init__(num_pools, headers, **connection_pool_kw)

if use_rdns:
self.pool_classes_by_scheme = {
'http': RdnsProxyHTTPConnectionPool,
'https': RdnsProxyHTTPSConnectionPool,
}
else:
self.pool_classes_by_scheme = {
'http': ProxyHTTPConnectionPool,
'https': ProxyHTTPSConnectionPool,
}

0 comments on commit 92d8aa5

Please sign in to comment.