Skip to main content

socksscope

PyPI Python License

A SOCKS5 front-end to better handle and restrict the scope of your traffic during an engagement.

Wraps a SOCKS5 proxy you already have via --upstream, or acts as a new SOCKS5 proxy with --local. Every CONNECT is judged against a ruleset of domain names, addresses and ports that is specified as arguments at startup.

socksscope additionally allows you to pick where domain names are resolved and which DNS server answers them. Through an existing SOCKS5 proxy only DNS via TCP can be used for this. See section 'DNS and SOCKS5' below.

This tool utilizes dnspython.

This is a pentesting tool. Only point it at systems and networks you are authorized to test.

Why this tool exists

  • Four reasons to be honest (which are also my personal use-cases)

    • During engagements I always end up with a lot of SOCKS5 proxies (ssh, sshcatch, chisel, ligolo, or the C2 directly) so sometimes it makes sense to restrict some of them to the network they belong to in order to prevent mistakes (especially when coworkers are involved)
    • The network at the other end of a SOCKS5 proxy requires some unique DNS settings (just some specific hosts or a specific DNS server) that I don't want all other SOCKS5 proxies to share and I don't want to (or cannot) reconfigure the jumphost
    • When using a tool via proxychains where I can't find a way to deactivate telemetry or other default requests that otherwise are sent via the proxy
    • Also I did some experiments with AI agents and wanted to be sure to restrict the network connectivity (similar to the first coworker case I guess)
  • My solution: socksscope

    • Open a (local) SOCKS5 proxy that wraps an existing SOCKS5 port or acts standalone
    • Allows you to restrict the connections made through it
      • Specify rules for domain names, addresses and ports on the command line or in a file
      • Rate limits and a connection cap that queues instead of dropping
    • Allows a flexible approach to DNS resolving
      • Specify a DNS server to use (also through the tunnel when DNS via TCP is available)
      • Specify domain name mappings like /etc/hosts

Install

pipx install socksscope     # or: pip install socksscope
socksscope -h

Or from source:

git clone https://github.com/LorenzMap/socksscope
cd socksscope
pip install .
./socksscope.py -h

It is a single file with one dependency (dnspython), so copying socksscope.py onto a host and installing dnspython works too.

Developed and tested on Python 3.12; needs at least 3.10.

Examples

Wrap an SSH dynamic forward, resolve domain names through it and block every connection except the ones in the private 10.0.0.0/8 network.

# Tunnel
ssh -ND 1080 user@jumphost

# Proxy
socksscope.py -l 1081 -u 1080 --dns u:10.0.0.53 --allow 10.0.0.0/8
13:10:11 listening on 127.0.0.1:1081 -> socks 127.0.0.1:1080, dns 10.0.0.53:53/tcp (upstream)
13:10:11   rule allow * (default)
13:10:11   rule allow :1-65535 (default)
13:10:11   rule allow 10.0.0.0/8
13:10:11
13:10:11 repeated connections to a host are counted, not logged
13:10:11 each host is summarised and reset 10s after it goes quiet
13:10:11 =====================
...

# Client (allowed if intranet.corp.local resolved to an address in 10.0.0.0/8)
proxychains curl http://intranet.corp.local/
# or
curl -x socks5h://127.0.0.1:1081 http://intranet.corp.local/

Allow web ports only on one domain name. WATCH OUT that this does not block IP address connections to the IPs that the domain resolves to! (To restrict that use explicit IP address rules or check the --resolve-rules example.)

socksscope.py -u 1080 --allow 'corp.local' --allow :80 --allow :443
13:11:24 listening on 127.0.0.1:1081 -> socks 127.0.0.1:1080, dns upstream
13:11:24   rule allow 0.0.0.0/0 (default)
13:11:24   rule allow ::/0 (default)
13:11:24   rule allow corp.local
13:11:24   rule allow :80
13:11:24   rule allow :443
13:11:24
13:11:24 repeated connections to a host are counted, not logged
13:11:24 each host is summarised and reset 10s after it goes quiet
13:11:24 =====================
...

Resolve the names on the local system but send the traffic through the tunnel.

socksscope.py -u 1080 --dns l:8.8.8.8 --allow '*.corp.local' --allow :443

Standalone with rate and connection count restrictions.

socksscope.py --local --allow 10.10.0.0/16 --rate 1M --max-conns 20

Check a scope file against a few targets before trusting it.

socksscope.py --allow @scope.txt \
              --test-ruleset admin.corp.local:445 \
              --test-ruleset 10.10.0.7 \
              --test-ruleset 8.8.8.8:53

Only allow connections to the specified domains and enforce that ALSO on IP addresses by resolving the domain name rules to their IP addresses.

socksscope.py -u 1080 --dns u:10.0.0.53 \
              --allow intranet.corp.local --allow fileserver.corp.local \
              --allow :443 --resolve-rules

How it works

Configure socksscope using command line arguments. See -h (short help) and --help (full help) as well as the examples section above.

Rules

With no rules at all nothing is restricted. --block rules obviously block all connections to the specified domain names, addresses or ports while everything else is allowed. On the other hand --allow rules block everything except the domain names, addresses or ports that are specified. Name, address and port are judged separately and all three have to pass. If multiple rules affect the same domain name, address or port, the rule that is the most specific wins (deeper subdomain, smaller subnet). An exact tie goes to block.

A rule is read from its shape:

value means
corp.local the domain name (and all its subdomains)
*.corp.local subdomains only (not the domain root)
* every domain name
10.0.0.0/8 a network
10.0.0.5 a single address
10.0.0.1-10.0.0.50 a range, both ends included
0.0.0.0/0 every IPv4 address
fe80::/64 a network
::/0 every IPv6 address
:443 a port
:8000-8100 a port range, both ends included

--allow and --block arguments are repeatable as many times as you want. You can specify a @FILE that loads a list of rules (one entry per line, # comments, ! to negate a rule).

# scope.txt given as '--allow @scope.txt'
corp.local
10.10.0.0/16
192.168.1.10-192.168.1.50
!192.168.1.42               # excluded host
:443

--test-ruleset shows the whole decision without opening a listener:

$ socksscope.py --local --allow 'corp.local' --allow :443 --block admin.corp.local \
    --test-ruleset www.corp.local --test-ruleset www.corp.local:443 --test-ruleset admin.corp.local:443
=====================

www.corp.local:80                  => DENY  (port not allowed by ruleset)
www.corp.local:443                 => ALLOW -> www.corp.local
admin.corp.local:443               => DENY  (block admin.corp.local)

When using --test-ruleset you may be asked whether DNS queries should be sent to resolve the domain names. Read the question, think about whether queries like that are acceptable in your engagement, and then answer.

DNS

This subsection describes what the different DNS arguments do. One important note first:

Where a domain name gets resolved decides how effective some rules are! For example, using an upstream SOCKS5 without specifying a --dns resolves the domain names on the upstream, so socksscope cannot enforce its address rules for that request. The section 'DNS and SOCKS5' below explains why this happens and why for this upstream use-case only DNS via TCP can be used.

If you want to skip that problem entirely and don't need domain name resolution at all: --block '*' refuses every domain name, so every request has to carry an address and the address rules apply to all of them.

--dns says which side of the tunnel a domain name is resolved on and, optionally, which server to ask over there:

--dns SIDE[:SERVER[:PORT][:tcp|udp]]

The side is u (upstream) or l (local). Without a SERVER the default system configuration of the specified side is used. If SERVER is specified socksscope queries the DNS itself.

--dns who answers the query goes transport
(nothing) u, or l with --local
u the upstream's resolver upstream's default configuration -
l local system's resolver local system's default configuration -
u:10.0.0.53 socksscope via 10.0.0.53 through the tunnel tcp
u:10.0.0.53:tcp socksscope via 10.0.0.53 through the tunnel tcp
u:[::1]:5353:tcp socksscope via ::1 on port 5353 through the tunnel tcp
u:10.0.0.53:udp refused, see 'DNS and SOCKS5' below
l:8.8.8.8 socksscope via 8.8.8.8 local system's network udp
l:8.8.8.8:udp socksscope via 8.8.8.8 local system's network udp
l:8.8.8.8:tcp socksscope via 8.8.8.8 local system's network tcp
l:8.8.8.8:5353 socksscope via 8.8.8.8 on port 5353 local system's network udp

SERVER must be an address, never a domain name. Resolving the resolver would need a resolver, and which side should answer that question is exactly the confusion this argument exists to remove. Look it up once yourself instead:

dig +short dns.corp.local                    # from here
proxychains dig +tcp +short dns.corp.local   # from the other end of the tunnel

Additionally: The tool you use through socksscope has to hand the domain name to the proxy instead of trying to resolve it itself. For example, use socks5h:// with curl and set network.proxy.socks_remote_dns = true in Firefox if you have trouble.

Hosts Mapping

--hosts is checked before all of the DNS handling and answers without any query. It takes mappings of name=ADDRESS or 'ADDRESS name [name ...]'. Files can be specified (similar to /etc/hosts).

--hosts intranet.corp.local=10.0.0.7
--hosts "10.0.0.8 db.corp.local db"
--hosts @hosts.txt

Resolve Rules

By default domain name rules only restrict the domain name and do not affect IP address rules. For example, a host may be blocked by a domain name rule but still be reachable by its IP.

--resolve-rules solves that. Every domain name rule is resolved at startup and again at a fixed interval (--resolve-rules-every, 300s by default) so socksscope can apply the addresses received as address rules. A blocked domain name then blocks its addresses too. Works with --test-ruleset as well. Resolving like this needs a resolver of our own, so anything but a plain --dns u. Wildcard rules (*.corp.local, *) cannot be resolved and stay name-only, socksscope says so at startup.

--resolve-rules results in active traffic to the specified DNS server! So it's probably best to use it only against public DNS servers, or internal ones where you know this is acceptable during an engagement. socksscope asks before it sends the first queries, --yes-resolve-rules answers that prompt for you.

Connection Limits

--rate is one budget shared by all connections, --rate-per-conn gives every connection its own. Both take bytes per second in a 1M or 512k style.

--max-conns caps how many connections run at the same time. The rest queue instead of failing, a waiting client simply sits in the SOCKS5 handshake without a reply until a slot frees up. --queue-timeout limits how long it waits there.

DNS and SOCKS5

While the tool feels intuitive in most aspects (at least to me), one stands out as confusing and I want to give some explanation for it.

The SOCKS5 protocol supports UDP, but most endpoints/servers do not (including ssh -D for example) (2026-08). Accordingly, DNS requests through SOCKS5 proxies are not sent as UDP requests from the client through the tunnel. Instead the domain name itself is sent in place of an IP address in the CONNECT request. The endpoint/server receives it, resolves the domain name using its own configuration or cache, and then establishes the TCP connection to the destination.

For the domain name rules and host to IP mapping of socksscope this is fine because we can simply read that domain name from the CONNECT request.

However, every time we want to actively resolve something through the tunnel using our own local logic we are limited to DNS via TCP requests. This affects the DNS queries of your tools as well as those from --resolve-rules, resulting in the limitation that --dns u:SERVER:udp is refused at startup. Asking over UDP means asking from here, which is --dns l:SERVER - the data still goes through the upstream, only the query does not.

Word of Warning

socksscope is not a firewall or privacy tool. It only sees what a client sends through it. Nothing stops a client from opening a socket directly, so the scope is enforced on the tools you point at it, not on the host. While it's possible to use socksscope securely, it is a pentesting/redteaming tool and NOT a privacy tool. There are a lot of ways to misconfigure socksscope!

--resolve-rules can be noisy. It queries every domain name rule again and again in fixed intervals. That is fine against a public DNS server, but may not be against an internal one that somebody is watching.

It fails closed. A domain name rule that cannot be resolved at startup stops the tool, and so does a domain name rule that stays unconfirmable for three refresh intervals. Better than judging traffic by an address we cannot verify.

Options

socksscope.py -h prints a short help with the arguments needed to get going. The full reference below is socksscope.py --help.

usage: socksscope.py [-h] [--help] [-l [HOST:]PORT] [--listen-auth USER:PASS]
                     [--local] [-u [HOST:]PORT] [--upstream-auth USER:PASS]
                     [-v] [-q] [--version] [--allow RULE] [--block RULE]
                     [--test-ruleset HOST[:PORT]] [--resolve-rules]
                     [--yes-resolve-rules] [--resolve-rules-every SEC]
                     [--dns SIDE[:SERVER[:PORT][:tcp|udp]]] [--hosts ENTRY]
                     [--rate SIZE] [--rate-per-conn SIZE] [--max-conns N]
                     [--queue-timeout SEC]

socksscope - a SOCKS5 front-end that lets you manage your traffic and keep it
inside your engagement scope. Can wrap an existing SOCKS5 port (--upstream)
or act independently (--local).

Every CONNECT is judged against a ruleset of domain names, addresses and ports,
given as arguments or in files at startup. Additionally you can specify a DNS
server to use and throttle connection speeds.

While it's possible to use socksscope securely, this is a pentesting/redteaming
tool and NOT a privacy tool. There are a lot of ways to misconfigure socksscope!
Watch out for unexpected rulesets when combining IP address and domain name rules.
(Check the README on Github for more information!)

options:
  -h                    show a short help message and exit
  --help                show the full help and exit
  -l [HOST:]PORT, --listen [HOST:]PORT
                        SOCKS5 port socksscope.py opens (default:
                        127.0.0.1:1081) - (restricted) SOCKS5 port where the
                        client programs connect to
  --listen-auth USER:PASS
                        optional credentials for the SOCKS5 port socksscope
                        opens
  --local               no SOCKS5 proxy to wrap, run socksscope.py
                        independently - connect out from this host while
                        enforcing the ruleset
  -u [HOST:]PORT, --upstream [HOST:]PORT
                        the existing SOCKS5 proxy that will be wrapped
                        (default: 127.0.0.1:1080)
  --upstream-auth USER:PASS
                        optional credentials for the upstream proxy
  -v, --verbose         log every connection with additional information
  -q, --quiet           log warnings only
  --version             show the version and exit

ruleset:
  --allow RULE          domain name, IP address or port rule to allow
                        (repeatable) - Rules: domain.tld | *.domain.tld | * |
                        IP | IP/NET | IP-IP | :PORT | :PORT-PORT - @FILE loads
                        a rules file - ranges include both ends - '!' inverts
                        a rule
  --block RULE          same as --allow but blocked instead (repeatable) -
                        syntax exactly like --allow
  --test-ruleset HOST[:PORT]
                        print how a target would be judged, then exit
                        (repeatable) - a target without a PORT is judged as
                        :80
  --resolve-rules       resolve domain name rules and apply them as IP address
                        rules (results in repeating queries)
  --yes-resolve-rules   answer the startup --resolve-rules confirmation with
                        yes
  --resolve-rules-every SEC
                        interval to re-resolve domain name rules (see
                        --resolve-rules) - if a domain name rule is
                        unconfirmed for three intervals socksscope exits - use
                        0 to resolve only once at startup (default: 300)

DNS:
  --dns SIDE[:SERVER[:PORT][:tcp|udp]]
                        specify where DNS queries should be resolved and what
                        protocol to use - check the README on Github for
                        explanations of all combinations - SIDE=[u|upstream]
                        to resolve through the wrapped SOCKS5 - SIDE=[l|local]
                        to resolve via the host socksscope is running on -
                        [:SERVER[:PORT]] optionally specify a DNS server -
                        [:tcp|:udp] optionally specify the DNS transport
                        protocol
  --hosts ENTRY         static mapping used before any DNS (like /etc/hosts)
                        (repeatable) - 'name=ADDRESS' or 'ADDRESS name' or
                        '@FILE' to load a list

limits:
  --rate SIZE           total bytes/s over all connections (e.g. 1M, 512k)
  --rate-per-conn SIZE  bytes/s for a single connection
  --max-conns N         connections to run at once - the rest queue instead of
                        failing
  --queue-timeout SEC   give up queueing after this long, 0 waits forever
                        (default: 60)

For more detailed examples, reasonings behind design decisions as well as an in-depth
explanation of socksscope's DNS resolving (especially when wrapping a SOCKS5 port and
actively resolving domain name rules using --resolve-rules) check the README on Github.

examples:
  socksscope.py -u 1080 --dns u:10.0.0.53 --allow @scope.txt
  socksscope.py -u 1080 --resolve-rules --allow 'intranet.corp.local' --allow :443
  socksscope.py --local --allow 10.0.0.0/8 --rate 1M --max-conns 20
  socksscope.py --allow @scope.txt --test-ruleset admin.corp.local:445

Testing

  • the test suite lives in tests/ (pytest)
  • run it from a virtualenv with the project and its dev dependencies installed (pip install -e ., then pip install --group dev on pip 25.1+ or simply pip install pytest pytest-xdist coverage)
  • run it via python -m pytest or tests/test.sh, -n 8 runs it in parallel
  • for the coverage of the tests run tests/test.sh cov
  • the throttling tests are timing based, so a busy machine can make them flap; re-run before believing a failure there

License

MIT

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

socksscope-0.2.0.tar.gz (30.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

socksscope-0.2.0-py3-none-any.whl (27.9 kB view details)

Uploaded Python 3

File details

Details for the file socksscope-0.2.0.tar.gz.

File metadata

  • Download URL: socksscope-0.2.0.tar.gz
  • Upload date:
  • Size: 30.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.3

File hashes

Hashes for socksscope-0.2.0.tar.gz
Algorithm Hash digest
SHA256 c168d6066aee3b4b1d17ee357291a7939c789bdca4d0bc8770f0ed7987872909
MD5 26d28c53d9e9ea2f996b9e03ccd13e9e
BLAKE2b-256 0a7d274f7ba2ce9465f762b1d9a7b8810e0709d19cc30da792e893632c8e2ae1

See more details on using hashes here.

File details

Details for the file socksscope-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: socksscope-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 27.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.3

File hashes

Hashes for socksscope-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 3df7c5dd938d71c5bf64959e84a66483a0757a9a9dd3248c6bcfc37f52fa3588
MD5 05eb3e61e9fd41ed7ca039362160fbb3
BLAKE2b-256 7000c14e8f55a6e14f36fe3edd665febd590d47339bd3b3436fe074417d9d1d4

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page