SOCKS proxy

Upcoming. This page describes master. The proxy is not part of a release yet, and details may change. Release 0.0.9 leaves it off: a daemon runs it only with the hidden --preview switch, which turns on the parts the release leaves off.

A Hedgehog daemon can run a local SOCKS5 proxy whose traffic leaves the Unigrid network at a gridnode, in a country the client may name. It serves CONNECT for TCP and UDP ASSOCIATE for UDP and refuses BIND. A destination can be an IPv4 or IPv6 address or a host name, which the exit looks up. Browsers, curl and any other SOCKS5 client can use it without knowing of gridnodes.

How it works

One proxy session: the SOCKS client, the local node, the members of a shard group, the exit and the destination

The gridnodes are dealt into shard groups of a few members each, anew every epoch. The members of a group keep a few append-only chains in memory, and several clients share each chain. Your node joins a group that has gridnodes in the country you ask for, writes your traffic to a chain through one or two of its members and reads the answers from the same chain. For every session it draws the exit among the members of that group in that country, other than those it writes through: the exit resolves the destination, connects to it and sends what comes back through the chain. A session that its exit does not answer within five seconds turns to another member, up to three in all, and the first of them to answer carries it.

What your node and the exit send each other is sealed with keys only the two of them hold, and nothing on the chain says which member is the exit or which country was asked for. The exit passes what it writes along a stem of other members before any member floods it, so the first member to flood it is seldom the exit.

The groups and their chains follow from the communication spork, the list of active gridnodes and the time, so every node finds the same ones without talking to the others. A session on a chain of the epoch that ended runs on for ten minutes, or for the length of an epoch if that is shorter, and is then closed at both ends.

Turning it on

The proxy is off unless the daemon runs with --preview and is given a port. These options go after daemon and are hidden from --help, for example hedgehog daemon --preview --socks-port 1080. Without --preview the daemon refuses a --socks-port other than 0 with --socks-port is not part of this release and exit code 2.

Option Default Meaning
--socks-port 0 Port of the proxy, 0 to 65535; 0 means no proxy
--socks-host 127.0.0.1 Address the proxy binds to; ignored while --socks-port is 0
--socks-country any country Country of the exit for a connection that names none, see Choosing the country
--socks-geoip geoip/country.mmdb in the data directory Country database in the MMDB format the gridnodes are looked up in

The proxy checks no credentials: the username only names a country. Bound to any address but a loopback one, it is open to everyone who can reach that address, and the daemon logs a warning saying so. A name for --socks-host that does not resolve, or an address and port that cannot be bound, is an error. The proxy holds at most 512 client connections at once and closes every one beyond that at once. A client has ten seconds for its greeting, its username and its command.

A gridnode (-G with its gridnode key) that the network lists as active takes part in the groups and can be the exit of a session. Any other node only joins a group as a client. A gridnode does not use the group it is a member of, so a network with a single group cannot be used from its own gridnodes.

A port alone does not switch the proxy on: until the network has a valid communication spork, every request is refused with reply 0x02. The board sets none on the Unigrid network until gridnodes prove collateral, so the proxy stays off there.

The communication spork

The communication spork, type COMMUNICATION with id 1060, switches the proxy on for the whole network and sets its parameters. Until a node holds a valid one, the proxy is off on that node, both for its own clients and as a member of a shard group. Like every grid spork it takes two board keys: one signs it and a second co-signs it, see Changing a spork. Only a node run with --preview takes it: one without drops it from a peer, POST /gridspork/wire answers 400 and GET /gridspork/communication 503, and the communication subcommands of gridspork-get and gridspork-set are hidden from --help.

Set it on a running daemon, which cli reaches at --resthost and --restport (localhost and 52884 unless given). One board key proposes the spork, with the fields below by name. A field left out takes its default, and the values shown are the defaults:

hedgehog cli gridspork-set communication --key-file <board key file> --first-version -D '{"groupSize":8,"reorganizePeriodSeconds":3600,"chainsPerGroup":4,"clientsPerChain":16,"chainCapacityBytes":33554432,"retentionSeconds":30,"maxEntryBytes":16384,"sessionIdleSeconds":300,"communicationFlags":3}'

--first-version is only for the first spork of the type, as the network holds none of it. hedgehog cli gridspork-pending lists the proposal, and hedgehog cli gridspork-cosign <digest> --key-file <other board key file> co-signs it, with --first-version COMMUNICATION for the first one, after which the node passes it on. hedgehog cli gridspork-get communication, or GET /gridspork/communication, prints the spork in force.

cli refuses a value outside the bounds below before it signs, and so does the daemon it sends the spork to. A node that holds a spork which breaks the bounds all the same, from a peer or from its spork file, keeps the proxy off. The flags are read when a connection or an association starts, so a change does not end what already runs, and sessionIdleSeconds applies to the sessions that start after the spork arrives. A change to any other field deals the groups and chains anew at once, and the sessions an exit relays on a chain whose chainCapacityBytes, retentionSeconds or maxEntryBytes changed end.

Field Default Allowed Meaning
groupSize 8 1 to 65535 Gridnodes per shard group. The active gridnodes are dealt evenly into as many groups as they fill at this size, and into one group when there are fewer
reorganizePeriodSeconds 3600 60 to 4294967295 Length of an epoch, after which the groups and chains are dealt anew
chainsPerGroup 4 1 to 65535 Chains each group hosts. A client is placed on the one with the fewest clients
clientsPerChain 16 1 to 65535 Clients a chain takes
chainCapacityBytes 33554432 (32 MiB) 4 × maxEntryBytes, and at least 32768, to 4294967295 Memory a chain may hold. While a chain is full, the TCP connections on it wait and the UDP datagrams a client sends are dropped
retentionSeconds 30 1 to 65535 How long entries stay on a chain
maxEntryBytes 16384 (16 KiB) 1024 to 1048576 Largest entry, its overhead included. A UDP datagram travels in one entry, so this also bounds a datagram
sessionIdleSeconds 300 1 to 65535 A session, TCP or UDP, without traffic for this long is closed
communicationFlags 3 0 to 3 Allowed commands as bits: 0x01 allows CONNECT, 0x02 allows UDP ASSOCIATE

Choosing the country

A connection names the country of its exit as its SOCKS username: an ISO 3166-1 alpha-2 code, in upper or lower case, such as de or SE. The password is not read. With curl:

curl --proxy socks5h://de:@127.0.0.1:<port> https://example.org
  • An empty username names no country. A connection that names none gets the country of --socks-country, or an exit in any country when that is not set. Browsers cannot send a SOCKS username, so they always get that default.
  • A username that is no country code is refused, and the client is disconnected.
  • A client that offers both no authentication and username/password is asked for the username. The JDK’s own SOCKS client offers both and then sends the name of the operating system user, which is refused, unless a java.net.Authenticator gives it an empty username or a country.

Your node finds the country of each gridnode itself, in a GeoIP database on your machine, and never asks an online service or a name server: only a gridnode that announces an IP address has a country. Any database in the MMDB format whose records hold country.iso_code works, such as DB-IP’s “IP to Country Lite”, which DB-IP publishes under CC BY 4.0 with attribution to DB-IP.com required. None comes with Hedgehog: put one at geoip/country.mmdb in the data directory or name it with --socks-geoip. A file over 64 MiB is refused. The node reads the file once, so a new one takes effect at the next start.

A connection that asks for a country fails with reply 0x03 when no database is loaded, or when no shard group has an active gridnode in that country.

Your node chooses the group and the exit itself. A group is drawn with a weight of its members in the country, so one with more of them is chosen more often, and the exit at random among those members. The country rests on the address a gridnode announces: an exit that has that address on its own host connects from it, but one behind NAT connects from another. A gridnode logs at INFO as it starts when its announced address is none of its host’s, as behind 1:1 NAT, or a loopback one, from which its exit reaches no public destination.

Using it from a client

Point any SOCKS5 client at the address and port of the proxy. With curl:

curl --socks5-hostname 127.0.0.1:<port> https://example.org

--socks5-hostname, or a proxy URL starting with socks5h://, hands the name to the proxy, so the exit looks it up and the name does not leak to the local resolver. --socks5 looks the name up on your own machine and sends the address. A browser takes the proxy as a SOCKS v5 host and port; let it send names through the proxy where it offers to.

The first connection after a start or a new epoch waits while the node joins a group. A connection or an association that no exit has answered after two minutes is refused with reply 0x06.

UDP needs a client that implements UDP ASSOCIATE. The proxy answers with the address of a datagram socket of its own, one for each association, on the address the client reached the proxy at. That socket takes datagrams only from the IP address of the client’s control connection, and from the port the client named in its request or, when it named port 0, from the port of the first valid datagram it sends. The exit sends the datagrams of an association from one socket of its own and passes on only the replies that come from an address and port the association sent to. The association ends with the TCP connection that asked for it, or when its session ends, as it does after sessionIdleSeconds without traffic.

UDP datagrams that cannot be passed on are dropped, as UDP allows:

  • fragments, and anything that is not a whole SOCKS5 UDP request;
  • one that does not fit one chain entry, about 16 KiB less its address with the default spork, and one the client sends while the chain is full;
  • one to port 0, and one to a destination beyond the 64 an association keeps at a time;
  • one to a host name while four others are being looked up or eight datagrams already wait for that lookup;
  • one to an address of the other IP family, when the exit sends from the address it announces;
  • one for the client that is longer than 65 507 bytes with its header, one that arrives while 64 already wait for the proxy’s event loop, and one that arrives before the client’s source is known.

Replies

The proxy answers each request with one SOCKS5 reply.

Reply Sent when
0x00 succeeded The exit connected, and the bound address is 0.0.0.0:0, so the client does not learn where the exit is; or the association is up, and the bound address is the proxy’s UDP socket
0x01 general failure The request cannot be read, or the connection ended before the exit replied
0x02 not allowed by ruleset No valid spork, its flag for the command is unset, or the exit refuses the destination
0x03 network unreachable No exit answered, no group took the node, a country was asked for with no database loaded or no group with an active gridnode in it, or a socket error at the exit
0x04 host unreachable The name has no address the exit can reach, or there is no route to it
0x05 connection refused The destination refused the connection
0x06 TTL expired The connection timed out at the exit, no answer came within two minutes, or the session went idle before the reply
0x07 command not supported BIND
0x08 address type not supported A destination the proxy does not accept, such as a name spelt as an IP address

Where traffic may go

An exit connects only to public addresses. For TCP and UDP alike it refuses loopback, private, link-local, multicast, documentation and the other special-purpose blocks, and IPv6 addresses that carry a refused IPv4 address; the blocks are listed on SOCKS proxy implementation.

It refuses localhost and every name below it without a lookup. A host name that ends in a number, such as 127.1 or 2130706433, or holds a colon, a bracket or a percent sign, is refused before anything looks it up, as resolvers read it as an address. Any other name is looked up once, refused when any of its addresses is refused, and reached only at the addresses that were checked. A UDP destination is looked up and checked again once its association has sent nothing to it for a minute. A destination whose address the policy refuses gets reply 0x02.

Who learns what

Who Learns Does not learn
Your node The exit, the country, the destination  
The members your node attaches through, one or two Your node’s address, and when and how much it sends The exit, the country, the destination
The other members of the group That writers write and how much; which entries are likely openings or answers; that the entries of one exit writer belong to one session The exit, the country, the destination
The exit The destination and, without TLS, what you send; the writer and the members your node uses Your node’s address
Other clients on the chain The sealed entries All of the above
Someone who watches an exit’s links That it answered an opening, as a packet of the smallest class leaves it for one member right after the opening arrives; there is no cover traffic, by design Your node’s address

What the protocol still gives away:

  • The first hop. The member an exit passes its entries to first learns that the member it got them from is the exit or relays for it, a better guess than chance.
  • The members your node attaches through. The exit takes the first two members that flood your writer’s entries to it for them, and passes them nothing first. When another member’s copy comes in before that of one of them, the exit never recognises that one, and its entries for your sessions may go to it first: it then has that guess about the exit as well as your node’s address.
  • Dropped entries. A member that drops the entries passed to it on the stem can tell whether the member that passed them wrote them, by who floods them and when: the exit floods after 4 seconds plus an exponential wait with a mean of 4, a member that only passed them on after 2 plus one with a mean of 2. These timers keep sessions alive, not private.
  • Stalled sessions. When the stem of a session leads back to its exit, the members on that loop race their timers for every entry, so the first to flood changes from entry to entry, and now and then it is the exit. In tests about 7 in 100 sessions stall so in a group of 8, and each of their entries waits 2 to 6 seconds.
  • Late stems. The signed timestamp of an entry tells the next member on the stem how long a stalled stem held it.
  • Traffic. There is a single hop: traffic does not pass from one shard group to another before it leaves. Members that compare notes over many entries, or watch their timing, can narrow down a busy exit. An attach member and the exit working together, or anyone who watches both ends of a session, can match them by timing and size.
  • Sessions fail closed. A session ends when its exit has no member left to pass its entries to, rather than have the exit flood them itself. Your node is not told and closes the session once it goes idle.

What to know before relying on it

  • The exit sees your traffic. It relays plaintext to the destination. Only TLS between your client and the destination, as with https://, keeps it from the exit, and keeps a hostile exit from reading or changing it.
  • Gridnodes are not proven. Nothing checks collateral yet, so any key can announce itself as a gridnode. Cheap fake gridnodes could weigh a group towards a rare country and take the sessions that ask for it.
  • Not a VPN. It is a proxy for the applications that are told to use it. There is no virtual network device and nothing is captured system-wide.
  • No half-close. When your client shuts down its sending side, the connection closes as a whole, and with it the session. A protocol that sends a request, shuts down its output and then waits for the answer gets none.
  • Writers can be used up. A chain holds at most 4096 writers, a writer being a client attached to it or one session of an exit. The writer of an exit session gives its place back once its last entry is older than retentionSeconds; any other keeps it until the epoch turns. Whoever can write to a chain can fill it with writers, and then no new client can attach to it and no exit can answer on it.
  • Lost entries. An entry your node missed is not fetched again; the connection it belonged to breaks once the missing entry is too old.
  • UDP with port 0. A client that names no port in UDP ASSOCIATE is identified by the first datagram from its IP address, so another program on that address that sends first is taken for the client. Keep the proxy on loopback on a machine you trust, or have the client name its port in the request.
  • Ports and the exit’s own network. The destination rules are all that protects the exit’s own host and the ports of the other gridnodes. There is no rule for ports, so a service that listens on the exit’s own public address can be reached through it.
  • Limits. The exit of a gridnode relays at most 256 sessions at once, at most 128 of them on one chain and 64 for one writer, and holds at most 256 writers of its sessions on one chain. It leaves an opening beyond that unanswered, so the client asks another member. It closes a session whose client sends nothing within 30 seconds of the exit’s answer, and one whose destination falls behind by more than 64 full entries of what the client sends. The local proxy drops a client that falls more than 1 MiB behind what its destination sends.

Peers and protocol versions

The proxy runs between nodes of this release started with --preview, as only they offer hedgehog-proxy/0.0.2. Two nodes agree on that protocol when they connect, or on hedgehog-ledger/0.0.2 when both run the ledger, which carries the proxy as well. Only a peer that agreed on one of them is sent the communication spork and carries chain traffic, on a second QUIC stream, stream 4.

A node of an earlier release, which offers only hedgehog/0.0.4 and gridspork/0.0.4, and one that offers hedgehog/0.0.9 first, a node of this release run without --preview or a build from before the proxy, still connect and exchange the other sporks, but take no part in the proxy: they are sent no communication spork and get no stream for chain traffic. See Peer-to-peer network protocol.

More

Page For
SOCKS proxy implementation Developers: the modules, the SOCKS5 front end, groups, countries and exits, the handshake and sealing, the exit side, the stem and the destination policy
SOCKS proxy chains Developers: chains, entries and their bytes, size classes, admission, the chain packets, joins, reconcile and stream budgets

Table of contents