Skip to content
fmotallebPublic

About

A simple configurable reverse proxy that uses socks and/or ssh as transport layer, with builtin support for SNI and Http-Header based autorouting.

Topics

Resources

Stars

4 stars

Watchers

1 watching

Forks

Latest commit

 

History

539 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Junction

build release CodeQL

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.


🌟 Features

  • 🔐 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.


📋 Table of Contents

  1. Getting Started
  2. Configuration
  3. Environment Variables
  4. Usage
  5. Development
  6. License

🚀 Getting Started

Installation

Standalone binary installation

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 | bash

or select a version manually:

VERSION=0.4.2 bash <<<"$(curl -fL https://raw.githubusercontent.com/fmotalleb/junction/refs/heads/main/install.sh)"

Using Go cli

Simply using

go install github.com/fmotalleb/junction@latest

in this method version variables are missing thus you cannot use --version (-v) to acquire version number

Docker based

Using:


❗️ Docker Image Details (Must Read)

contains junction itself based on distroless images by google

  • latest
  • latest-distroless
  • {{ .Version }}-distroless

Run Docker container

# 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.toml

🛠 Configuration

Configuration

Remember 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

Run SubCommand

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

Fields

  • 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=1 is set temporarily.
    include = [
      "./*.toml",
      "https://remote-server.com/config.toml",
    ]
  • Core Some specific global configurations are stored here

    • pprof.enable: set to true to serve Go profiling endpoints.
    • pprof.listen: listener address, such as 127.0.0.1:6060 or :6060. Defaults to 127.0.0.1:6060; the pprof index is at http://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; set listen explicitly to choose another address. Hardened mode requires allow_from rules 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 value

          • answer: Single IPv4 to answer
          • from: 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^")
        • 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 to 127.0.0.1:port)
      • Integer: port (binds to 127.0.0.1:port)

      In hardened mode, unrestricted non-loopback sni, http-header, and http-static listeners must set public_access = true or define allow_from rules. In the default compatibility mode, listeners start without that opt-in.

    • routing (required): Target hostname resolution method:

      • sni: Uses SNI for hostname detection. Default port: 443
      • http-header: Uses HTTP Host header. Default port: 80
      • http-static: Serves a configured templated response without proxying the request
      • tcp-raw: Raw TCP forwarding. Requires complete ip:port in to field
      • udp-raw: Raw UDP forwarding. Requires complete ip:port in to field. Proxy chains are not implemented and fall back to direct traffic by default. UDP_BUFFER controls 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, or http-to-https) - specify the same tag identifier Tags are not supported for tcp-raw or udp-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_hosts or the known_hosts query 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"]
      graph 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"]
      
      Loading
    • to (required except for http-static): Target destination:

      • For sni/http-header: Port number (string)
      • For tcp-raw/udp-raw: Complete "ip:port" address
      • Not used by http-static
    • response_headers, content, and status_code (for http-static): response_headers is a map of response header names to values. Header values and content support Go templates. The template data provides .URL, a net/url.URL built from the request, and .Method. For example, .URL.Hostname, .URL.Port, .URL.String, .URL.Scheme, .URL.Path, and .URL.RawQuery are available. The body uses html/template escaping. status_code is optional and defaults to 200.

      [[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, and status_code (for http-static): The response body and headers support Go templates with .URL and .Method. status_code defaults to 200. To redirect every request to HTTPS on port 443, set a redirect status and render the request path and query into Location:

      [[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 (or TIMEOUT environment variable)
      • Format: Go duration syntax (e.g., "50s", "5h3m15s")
    • features (optional): List of feature flags that enable routing-specific behavior.

      • http-header:
        • flexible-port: Allows per-request port override using the Junction-Port HTTP header. When allowed_ports is set, requests for other ports are rejected. Hardened mode requires an explicit allowed_ports list. If the header is missing or empty, the to value (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")
    • 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
    • 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
    • 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
    • destination_allow and destination_block (optional): CIDR policy applied to resolved destination addresses on direct routes. If destination_allow is set, only addresses in those ranges are dialed; destination_block always 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=1 to opt into the security and resource controls. See HARDENING.md for details.

  • Proxy chains execute in order; incorrect ordering breaks the chain

  • tcp-raw and udp-raw require explicit ip:port targets

  • udp-raw routing 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

Example: TOML Configuration

[[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 directly

Example: YAML Configuration

entrypoints:
  - 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:7890

JSON 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 from stdin using toml format --dry-run writes 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 mode 0600. Use --dry-run --redact-secrets for a redacted diagnostic. Secret values are replaced, so this output is not suitable for restarting Junction.


▶️ Usage

Running Locally

  1. Build the Go application:

    go build -o junction
  2. Run the application:

    ./junction --config=config.toml

Running with Docker

To build and launch the Docker container:

docker-compose up --build

Once running, the application will be accessible on the configured ports.


🛠 Development

Profiling relay goroutines

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.txt

The 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.

Debugging with VS Code

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.toml

Directory Structure

Junction'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

📜 License

This project is licensed under the GNU General Public License v2.0. Refer to the LICENSE file for more details.

About

A simple configurable reverse proxy that uses socks and/or ssh as transport layer, with builtin support for SNI and Http-Header based autorouting.

Topics

Resources

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages