Junction is a lightweight reverse proxy optimized for efficient TCP and TLS traffic routing. It inspects protocol-level metadata (such as SNI in TLS) to forward encrypted connections to the appropriate backend, without decrypting the traffic. Junction supports both SOCKS5 and SSH proxy protocols (and chaining them), making it ideal for complex egress scenarios where transparent, performant routing is required.
-
🔐 Sni Passthrough
No certificate required, reroutes tls packets using sni header. -
🧦 SOCKS5 Proxy Support
Routes traffic using SOCKS5 proxies. -
🔀 SSH Proxy Support
Routes traffic using SSH connection as proxy. -
🔗 Proxy Chain Support
Chain multiple proxies together to create complex routing paths and improve privacy or bypass restrictions. -
🐳 Dockerized Deployment
Includes a ready-to-use Docker setup for seamless deployment in any environment. -
🌐 Internal Fake DNS
Fake DNS with ability to match only some domains, change result based on client's ip address, and forward unmatched requests to an optional forwarder DNS.
You can grab one of builds from Release page
or use the shell script (please review the scripts before executing it in your shell, or any script you find online who paste them in the shell without checking)
This script requires curl, tar, jq (if version is missing), sha256sum (optional but recommended) and bash itself.
Install latest version (requires jq):
bash <<<"$(curl -fL https://raw.githubusercontent.com/fmotalleb/junction/refs/heads/main/install.sh)"curl -fL https://raw.githubusercontent.com/fmotalleb/junction/refs/heads/main/install.sh | bashor select a version manually:
VERSION=0.4.2 bash <<<"$(curl -fL https://raw.githubusercontent.com/fmotalleb/junction/refs/heads/main/install.sh)"Simply using
go install github.com/fmotalleb/junction@latestin this method version variables are missing thus you cannot use --version (-v) to acquire version number
Using:
contains junction itself based on distroless images by google
latestlatest-distroless{{ .Version }}-distroless
# Documented example of config file
docker run --rm -it ghcr.io/fmotalleb/junction:latest example
# Save config file to
docker run --rm ghcr.io/fmotalleb/junction:latest example > config.toml
docker run --rm -it \
-v "./config.toml:/config.toml" \
--network host \ # or map each port manually
ghcr.io/fmotalleb/junction:latest -c /config.tomlRemember that the cli has an example sub command that will be updated more than this section,
Config specifications defined here may not be complete
Most parameters are loosely typed
arrays can be defined as single items and it will be mapped to array internally
some objects are able to parse themselves from strings
Simplest way to run the server is using run sub command
junction run --help # show help for this sub command
# Simple example of run command that listens on port 8443
# thru socks5 proxy on port 7890 of localhost
# transfers the request to port 443
# of the found hostname using `sni` packets
junction run --listen 8443 \
--proxy socks5://127.0.0.1:7890 \
--target 443 \
--routing sni-
Include You can include multiple config files, including remote HTTP or HTTPS sources. Include files are merged in listed order; the including file overrides included values. Missing or invalid includes are skipped by default. With
JUNCTION_HARDENED=1, include failures abort loading and remote sources require HTTPS. Parsing builds a fresh configuration snapshot, so removed fields do not carry over from a previous load.- Support Glob pattern matching
- Remote sources support basic authentication. HTTPS is recommended. In
hardened mode, plain HTTP is rejected unless
JUNCTION_ALLOW_INSECURE_CONFIG_HTTP=1is set temporarily.
include = [ "./*.toml", "https://remote-server.com/config.toml", ]
-
Core Some specific global configurations are stored here
pprof.enable: set totrueto serve Go profiling endpoints.pprof.listen: listener address, such as127.0.0.1:6060or:6060. Defaults to127.0.0.1:6060; the pprof index is athttp://127.0.0.1:6060/debug/pprof/with the default address.
TOML:
[core.pprof] enable = true listen = "127.0.0.1:6060"
YAML:
core: pprof: enable: true listen: "127.0.0.1:6060"
- fake_dns: object or string of fake DNS config:
In order to create a simple dns server to manipulate requests into this server (needs manual IP configuration)
By default the DNS listener binds to all interfaces on port 53. In hardened
mode it binds to
127.0.0.1:53; setlistenexplicitly to choose another address. Hardened mode requiresallow_fromrules for non-loopback listeners. Keep network firewall rules in place as well. Limitations:- A Record support (for now since i don't use AAAA records)
- UDP listener; DNS-over-TCP clients are not supported
- UDP upstream forwarding with automatic TCP retry when the upstream marks
its UDP response truncated
Config:
-
listen: UDP listen address, requires a UDP ip:port -
answer: IPv4 Answer, requires a single IPv4 address alternatively this field is able to receive array(or just an object) as valueanswer: Single IPv4 to answerfrom: CIDR address of remote user that queries DNS Order is important on masking networks
[[core.fake_dns.answer]] answer = "127.0.0.1" from = "127.0.0.1/24" [[core.fake_dns.answer]] answer = "192.168.1.1" from = [ "192.168.2.0/24", "128.1.1.0/24" ]
-
forwarder: Upstream DNS server for unresolvable/not-allowed queries, (e.g.8.8.8.8:53), if omitted will return empty response -
allowed: Allowed list matcher- Supports wildcards (e.g.,
"*.example.com") - Supports Regular Expression (e.g.
"regex:allowed","grep:.+google.com^")
- Supports wildcards (e.g.,
-
allow_from: Required for non-loopback listeners; matcher rules apply to client IP addresses (for example"192.168.1.*"or"regex:^10\\.").
-
- singbox: object of singbox config singbox is a successor to xray Its config is complex you can see an example of how to provide a simple config in example directory
-
Entrypoints: Top-level array defining routing configurations. Each entry includes:
-
listen(required): Bind address for incoming connections. Accepts:- Full address:
"IP:port"(e.g.,"0.0.0.0:8443") - Port only:
":port"(binds to127.0.0.1:port) - Integer:
port(binds to127.0.0.1:port)
In hardened mode, unrestricted non-loopback
sni,http-header, andhttp-staticlisteners must setpublic_access = trueor defineallow_fromrules. In the default compatibility mode, listeners start without that opt-in. - Full address:
-
routing(required): Target hostname resolution method:sni: Uses SNI for hostname detection. Default port:443http-header: Uses HTTPHostheader. Default port:80http-static: Serves a configured templated response without proxying the requesttcp-raw: Raw TCP forwarding. Requires completeip:portintofieldudp-raw: Raw UDP forwarding. Requires completeip:portintofield. Proxy chains are not implemented and fall back to direct traffic by default.UDP_BUFFERcontrols the upstream response buffer (default 65507 bytes). Hardened mode rejects proxy chains and validates buffer limits.- Hardened mode caps TCP, SNI, and HTTP listeners at 1024 concurrent connections each, with a per-source cap of 64; UDP sessions are capped at 4096 per listener and 64 per source IP.
-
tag(optional): The tag attribute groups multiple entrypoints so they share a single listening socket while applying different domain-matching rules. A tag represents a routing group evaluated on the same port. Rules are checked in configuration order; the first matching entrypoint is used. All entrypoints in a tag group must: - use the same listen address - use the same routing mode (sni,http-header,http-static, orhttp-to-https) - specify the same tag identifier Tags are not supported fortcp-raworudp-raw. -
proxy(optional): Upstream proxy configuration. Accepts:- String: Comma-separated proxy chain
- Array: Ordered list of proxy URIs
Supported proxy protocols:
- SOCKS5:
socks5://[user:pass@]hostname:port - SSH:
ssh://user[:pass]@hostname:port[/path/to/private/key]?known_hosts=/path/to/known_hosts- Use either password OR key authentication, not both
- SSH host-key verification is enabled by
JUNCTION_HARDENED=1. Configure~/.ssh/known_hostsor theknown_hostsquery parameter when using it.
Default:
direct(no proxy). Unsupported schemes are ignored by default and may leave the chain direct; hardened mode rejects them.Example proxy chains (equivalent):
"socks5://user:pass@10.0.0.1:1080,socks5://10.0.0.2:1080,ssh://user@10.0.0.3:22/tmp/key"["socks5://user:pass@10.0.0.1:1080", "socks5://10.0.0.2:1080", "ssh://user@10.0.0.3:22/tmp/key"]
Loadinggraph LR Client --> Proxy1["socks5://user:pass@10.0.0.1:1080"] Proxy1 --> Proxy2["socks5://10.0.0.2:1080"] Proxy2 --> Proxy3["ssh://user@10.0.0.3:22"] Proxy3 --> Target["example.com:80"]
-
to(required except forhttp-static): Target destination:- For
sni/http-header: Port number (string) - For
tcp-raw/udp-raw: Complete"ip:port"address - Not used by
http-static
- For
-
response_headers,content, andstatus_code(forhttp-static):response_headersis a map of response header names to values. Header values andcontentsupport Go templates. The template data provides.URL, anet/url.URLbuilt from the request, and.Method. For example,.URL.Hostname,.URL.Port,.URL.String,.URL.Scheme,.URL.Path, and.URL.RawQueryare available. The body useshtml/templateescaping.status_codeis optional and defaults to200.[[entrypoints]] routing = "http-static" listen = "127.0.0.1:8085" response_headers = { "Content-Type" = "text/plain; charset=utf-8", "X-Request-Host" = "{{ .URL.Host }}" } content = "host={{ .URL.Hostname }} port={{ .URL.Port }} url={{ .URL.String }}"
-
content,response_headers, andstatus_code(forhttp-static): The response body and headers support Go templates with.URLand.Method.status_codedefaults to200. To redirect every request to HTTPS on port443, set a redirect status and render the request path and query intoLocation:[[entrypoints]] routing = "http-static" listen = "127.0.0.1:8080" status_code = 302 response_headers = { "Location" = "https://{{ .URL.Hostname }}:443{{ .URL.RequestURI }}" } content = ""
-
timeout(optional): Connection timeout duration:- Default:
24h(orTIMEOUTenvironment variable) - Format: Go duration syntax (e.g.,
"50s","5h3m15s")
- Default:
-
features(optional): List of feature flags that enable routing-specific behavior.http-header:flexible-port: Allows per-request port override using theJunction-PortHTTP header. Whenallowed_portsis set, requests for other ports are rejected. Hardened mode requires an explicitallowed_portslist. If the header is missing or empty, thetovalue (or the router default) is used.- HTTP requests default to port
80. Hardened mode validates targetless CONNECT authorities and uses the authority port.
sni:sni-fragment-support: Reassembles TLS ClientHello messages split across records. Without this feature, SNI routing uses the legacy single-read parser and 4 KiB buffer.
- Other routers currently ignore feature flags (unknown features are ignored with a warning).
-
block_list(optional) [for sni, http-header, and http-static]: List of hostnames/patterns to block.- Supports wildcards (e.g.,
"*.example.com","glob:*.example.com") - Supports Regular Expression (contain check) (e.g.
"regex:badword","grep:bad.+word")
- Supports wildcards (e.g.,
-
allow_list(optional) [for sni, http-header, and http-static]: List of hostnames/patterns to allow. If specified, only listed hosts are allowed.- Supports wildcards (e.g.,
"*.example.com") - Supports Regular Expression (e.g.
"regex:allowed","grep:.+google.com^") - Block rules are applied before allow rules
- Supports wildcards (e.g.,
-
block_from(optional): List of client address patterns to block (applies to all routers).- Supports wildcards and regular expressions (same matcher rules as
block_list) - Block rules are applied before allow rules
- Supports wildcards and regular expressions (same matcher rules as
-
allow_from(optional): List of client address patterns to allow (applies to all routers). If specified, only listed clients are allowed.- Supports wildcards and regular expressions (same matcher rules as
allow_list) - Block rules are applied before allow rules
- Supports wildcards and regular expressions (same matcher rules as
-
destination_allowanddestination_block(optional): CIDR policy applied to resolved destination addresses on direct routes. Ifdestination_allowis set, only addresses in those ranges are dialed;destination_blockalways takes precedence. The proxy resolves the hostname and dials the checked address directly. These rules cannot be combined with a proxy chain because the chain may resolve destinations remotely; startup fails if both are configured.
-
Important Notes:
-
Set
JUNCTION_HARDENED=1to opt into the security and resource controls. SeeHARDENING.mdfor details. -
Proxy chains execute in order; incorrect ordering breaks the chain
-
tcp-rawandudp-rawrequire explicitip:porttargets -
udp-rawrouting doesn't support proxy protocols -
When using
allow_list, unlisted hosts are implicitly blocked -
When using
allow_from, unlisted clients are implicitly blocked -
Wildcard patterns (e.g.,
*.example.com) match subdomains only, not the base domain
[[entrypoints]]
listen = "0.0.0.0:8443" # Listen IP:Port address
public_access = true # Needed for unrestricted public access in hardened mode
to = "443" # Reroutes connections to this port (defaults to 443)
routing = "sni" # Routing method
proxy = "socks5://127.0.0.1:7890" # socks5 proxy address
[[entrypoints]]
listen = ":8080" # Listen on 127.0.0.1:8080
routing = "http-header"
to = "80" # Defaults from `Host`
proxy = "socks5://127.0.0.1:7890"
features = ["flexible-port"] # Allow per-request port override via Junction-Port header
allowed_ports = ["80", "443"] # Restricts Junction-Port; required in hardened mode
allow_from = ["127.0.0.1", "10.0.*", "regex:^192\\.168\\.1\\."]
[[entrypoints]]
listen = 8090 # Listen on 127.0.0.1:8090
routing = "http-header"
to = "80"
proxy = "direct" # Do not handle using proxy just reverse proxy it directly
[[entrypoints]]
listen = 8099
to = "18.19.20.21:22" # Required for tcp-raw
routing = "tcp-raw" # TCP raw is old behavior where the target address must be specified (used for non-tls non-http requests that do not have any indications for server name nor address)
proxy = "direct" # Do not handle using proxy just reverse proxy it directlyentrypoints:
- routing: "sni" # Routing method
listen: 8443 # Listen ip addr (default ip is 127.0.0.1 if omitted)
to: "443" # Reroutes connections to this port (defaults to 443)
proxy: socks5://127.0.0.1:7890 # socks5 proxy address
- routing: http-header
listen: 8080
to: "80" # Defaults to 80
proxy: socks5://127.0.0.1:7890JSON Schema for the configuration file is available at config.schema.json.
You can specify config file path using
--config (-c)flag (detects config file) Default behavior is to read config fromstdinusingtomlformat--dry-runwrites the full configuration to stdout, including passwords and secret fields. Treat it as an export, not a redacted diagnostic; if saving it, use a private file owned by the service user with mode0600. Use--dry-run --redact-secretsfor a redacted diagnostic. Secret values are replaced, so this output is not suitable for restarting Junction.
-
Build the Go application:
go build -o junction
-
Run the application:
./junction --config=config.toml
To build and launch the Docker container:
docker-compose up --buildOnce running, the application will be accessible on the configured ports.
Enable core.pprof.enable and capture an aggregated goroutine profile from the
configured listener:
curl -s 'http://127.0.0.1:6060/debug/pprof/goroutine?debug=1' > goroutines.txtThe debug=2 stack dump is capped at 64 MiB by Go and can end mid-stack when there
are many goroutines. Use debug=1 or the binary profile without a debug query
parameter to capture the complete profile.
SNI, raw TCP, and HTTP CONNECT relays close both endpoints on the first EOF, I/O error, or context cancellation, then wait for both copy workers to exit. A TCP half-close also shuts down the entire tunnel. A peer that disappears without a FIN/RST, or an EOF hidden behind a blocked write, may only be detected by TCP keepalive or the entry timeout. The entry timeout bounds total tunnel lifetime; it is not an idle timeout.
Create a local .vscode/launch.json (it is git-ignored) with a "Launch Package"
configuration, or start under Delve directly:
dlv debug ./main.go -- --config=config.tomlJunction's project structure is organized as follows:
.
├── cmd/ # CLI entry point
├── config/ # Configuration parsing and helpers
├── connection/ # TCP/UDP connection and idle-cleanup helpers
├── crypto/ # ClientHello parser used for SNI routing
├── dns/ # FakeDNS server and request forwarding
├── example/ # Example configuration files
├── internal/ # Embedded configuration UI (Vite/React)
├── proxy/ # SOCKS5 and SSH dialers
├── router/ # Routers (sni, http-header, tcp-raw, udp, ...)
├── server/ # Server lifecycle and entrypoint wiring
├── utils/ # Shared helpers (copy, ...)
├── Dockerfile # Container image build
├── docker-compose.yml # Docker Compose configuration
└── main.go # Main entry point
This project is licensed under the GNU General Public License v2.0. Refer to the LICENSE file for more details.