Request lifecycle
Real traffic through the code: one TCP connection through a SOCKS5 inbound and freedom, one UDP session from SOCKS5 UDP ASSOCIATE, and the same through a Trojan-over-TLS proxy. Every function named here exists.
Startup
cmd/apranik/cli/run.go does the wiring:
loadcallsregistry.Default()andconfig.LoadFile, which parses the file into ablueprint.Blueprintand checks it.servecreates abuilder.Builder, which starts logging.BuildOutboundbuilds each outbound:outbound.NewFreedomoroutbound.NewStream.BuildRouterloads the datasets and compiles the rules once; one router serves every inbound.BuildInboundbuilds each inbound, andActivatestarts it with a handler.
One TCP connection
Step by step:
- Accept.
streamInbound.acceptLoop(core/inbound/inbound.go) accepts, takes amax_tcp_connectionsslot and startshandleStreamin a goroutine. - Overlays.
wrapServerOverlaysruns each overlay'sWrapServerStreamin stack order (TLS, then WebSocket), each under its ownhandshake_timeout. An overlay that learns the real client replacesSourcein thespec.TransportMetadata. A failure closes the connection. - Proxy handshake. The proxy server's
AcceptStream(for SOCKS5,module/proxy/socks5/server.go) authenticates, reads the destination and returns the payload stream with itsMetadata. A protocol that also carries UDP is first askedIsDatagramOverStream, which only peeks. A refusal withspec.ErrFallbackRequestedreplays the bytes to the inbound'sfallbackinstead. - Route.
Handler.HandleStream(core/route/handler) wraps the stream in aninformant.Streamand callsRouter.Route, which returns aDecision: an outbound, or block. - Sniff, when a rule asks. Only then is the stream made a
peekable.Stream;sniff.Recognisepeeks under a read deadline, and the peeked bytes are relayed later. The result fillsMetadata.Sniffed; the destination never changes. - Dial.
freedomOutbound.DialStreamhands the destination totcp, which resolves a domain throughcore/dnsand dials with Happy Eyeballs. A proxy outbound dials its server instead and calls the client'sRequestStream: here the address stops meaning the destination and starts meaning the proxy. - Relay.
relay.Stream(core/relay) copies both ways and half-closes the far side when one side ends. Cancellation ortcp_idle_timeoutcloses both.
Sniffing in detail:
One UDP session (SOCKS5 UDP ASSOCIATE)
Step by step:
- Hand-off. SOCKS5's
AcceptStreamreturnsspec.ErrUDPAssociationRequested;handleUDPAssociatecallsAssociate, which opens the second socket through thespec.UDPAssociateSocketFunccore granted. - Lifetime. A goroutine reads the control stream; when it closes, the UDP session ends.
- Session.
Handler.HandleDatagramcreates asession.Sessionand a routing cache, then runsrelay.Datagrams. - Client to destination. Each packet's destination is routed once and the decision cached, block included.
Session.Sendwrites it; a packet that cannot be routed or sent is dropped and the session goes on. - Outbound datagram. The first send through an outbound calls its
CreateDatagram; for freedom,nat.New, which opens sockets as its mapping policy says. - Replies. Replies from every outbound datagram merge into one channel and go back to the client, under the domain the client used.
- End. The client closes, or
udp_session_idle_timeoutpasses in silence. Freedom's NAT sockets close afternat.idle_timeout.
Through a proxy outbound over TLS
On the client, a Trojan-over-TLS outbound replaces the dial:
streamOutbound.DialStream(core/outbound/proxy.go) calls the dialer the builder folded from the stack: TCP, then the TLS client'sWrapClientStream.- The Trojan client's
RequestStreamreturns at once; the request leaves with the first payload, or alone afterproxy_request_timing.max_delay. - On the server, TLS's
WrapServerStreamruns in the connection's goroutine, and Trojan'sAcceptStreamchecks the password and reads the destination. The rest is the same as above.
For UDP, CreateDatagram makes the same dial and calls RequestDatagramOverStream, so the whole UDP session is framed in one stream. An outbound whose protocol cannot frame UDP, such as SOCKS5, lists only tcp in Networks(), and UDP routed to it is dropped.