spaceship

module
v2.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 23, 2026 License: GPL-3.0

README

Spaceship

Spaceship is a tool designed to create secure tunnels to remote networks.

Technologies Used

  • gRPC
  • Protocol Buffers (protobuf)

Usage

# spaceship -h
Usage of spaceship:
  -c string
        config path (default "./config.json")
  -interval duration
        show stats interval in seconds (default 1s)
  -s    show stats
  -v    show spaceship version

Linux TCP Transparent Redirect

On Linux, a client can accept TCP connections sent to it by the REDIRECT target in the iptables or ip6tables nat table. Enable the listener with listen_redirect:

{
  "role": "client",
  "server_addr": "tunnel.example.com:443",
  "tls": true,
  "uuid": "00000000-0000-0000-0000-000000000001",
  "listen_redirect": "0.0.0.0:12345",
  "redirect": {
    "max_connections": 1024,
    "bypass_mark": 21328
  }
}

The listener reads SO_ORIGINAL_DST from each accepted socket, routes the recovered destination IP through the configured Spaceship routes, and carries the TCP stream through the selected egress. It is Linux-only and TCP-only. Because REDIRECT supplies an IP address rather than a hostname, cidr, exact IP, and default routes are useful here; domain routes cannot match unless a separate future traffic-sniffing feature recovers a hostname.

redirect.max_connections bounds accepted sessions and their proxy goroutines. Omit it or set it to 0 to use the default of 1024; the accepted maximum is 65536, because each admitted session owns a socket, a goroutine, and an egress stream. When the limit is reached, new connections remain in the kernel listen backlog until capacity is available; size the limit together with the service file-descriptor limit and expected tunnel capacity. The redirect section configures that listener, so it is rejected without listen_redirect rather than silently ignored.

For traffic forwarded from a LAN, put rules in a dedicated chain and scope the jump to the intended ingress interface. Adapt the interface, exclusions, and port to the host:

iptables -t nat -N SPACESHIP_REDIRECT
iptables -t nat -A SPACESHIP_REDIRECT -d 127.0.0.0/8 -j RETURN
iptables -t nat -A SPACESHIP_REDIRECT -m addrtype --dst-type LOCAL -j RETURN
iptables -t nat -A SPACESHIP_REDIRECT -p tcp -j REDIRECT --to-ports 12345
iptables -t nat -A PREROUTING -i lan0 -p tcp -j SPACESHIP_REDIRECT

For traffic originating on the Spaceship host, exempting Spaceship's own egress is mandatory. The listener cannot distinguish a newly redirected application flow from Spaceship's own outbound gRPC or direct egress by destination alone. Without an exemption the listener captures its own egress, routes it, captures the result, and repeats: a single connection exhausts max_connections in well under a second and the frontend stops accepting traffic.

Spaceship marks its own egress for you: enabling listen_redirect applies redirect.bypass_mark — decimal 21328 (0x5350) by default — with SO_MARK to its gRPC control connection, direct TCP/UDP egress, forward-proxy connection, and pure-Go resolver sockets. Exempt that mark. Prefer it over -m owner, which silently fails to match when Spaceship runs as root. Keep the owner rule too, as defense in depth, along with the tunnel server address and local control-plane networks:

iptables -t nat -N SPACESHIP_LOCAL
iptables -t nat -A SPACESHIP_LOCAL -m mark --mark 0x5350/0xffffffff -j RETURN
iptables -t nat -A SPACESHIP_LOCAL -m owner --uid-owner spaceship -j RETURN
iptables -t nat -A SPACESHIP_LOCAL -d 127.0.0.0/8 -j RETURN
iptables -t nat -A SPACESHIP_LOCAL -m addrtype --dst-type LOCAL -j RETURN
iptables -t nat -A SPACESHIP_LOCAL -d 192.0.2.10/32 -j RETURN
iptables -t nat -A SPACESHIP_LOCAL -p tcp -j REDIRECT --to-ports 12345
iptables -t nat -A OUTPUT -p tcp -j SPACESHIP_LOCAL

SO_MARK needs network-administration capability (normally CAP_NET_ADMIN), which a LAN-only PREROUTING deployment does not otherwise require — nothing there captures locally-originated traffic. So when the mark is only the default and the process cannot apply it, Spaceship warns at startup and continues unmarked rather than refusing to run. Setting bypass_mark explicitly makes it a requirement instead: startup then fails with one clear error if it cannot be applied. Set it to 0 to disable marking deliberately and silence the warning. When TUN is also enabled its bypass_mark is inherited automatically; setting a different value here is rejected, because a process has exactly one outbound socket mark.

Replace 192.0.2.10 with every IP used by server_addr; do not use the documentation address literally. Add explicit exclusions for management and other networks that must stay local.

For IPv6 interception, set "ipv6": true, listen on [::]:12345, and install equivalent ip6tables rules. Without "ipv6": true, Spaceship intentionally installs an IPv6 block route. Exclude ::1/128, multicast, and link-local destinations unless they deliberately use a direct route; a link-local scope identifier is meaningful only in the local network namespace.

SO_ORIGINAL_DST depends on a conntrack entry. The REDIRECT rule and Spaceship listener must run in the same network namespace, with IPv4/IPv6 conntrack and NAT support available. In production, persist rules with the host's firewall manager or iptables-restore, restrict the listener to trusted ingress, and remove the jump before deleting or changing its chain. Verify the dedicated UID exemption and tunnel-server exclusions before enabling the OUTPUT jump.

On a Linux build host with unshare, ip, and iptables, the opt-in test below creates a disposable network namespace and validates the real conntrack, SO_ORIGINAL_DST, routing, and TCP stream path. Add the second environment variable to cover IPv6 with ip6tables:

SPACESHIP_REDIRECT_INTEGRATION=1 \
SPACESHIP_REDIRECT_INTEGRATION_IPV6=1 \
go test -count=1 -run '^TestNetfilterRedirectIntegration$' ./internal/redirect

Linux TUN Frontend

The client can terminate IP packets from a Linux TUN interface in its own gVisor network stack. Spaceship implements this frontend directly; it does not embed or invoke tun2socks. General TUN traffic is TCP-only. When DNS hijacking is enabled, UDP is admitted only for destination port 53 so classic DNS works without enabling a general UDP tunnel.

{
  "role": "client",
  "server_addr": "tunnel.example.com:443",
  "tls": true,
  "uuid": "00000000-0000-0000-0000-000000000001",
  "ipv6": true,
  "mux": 2,
  "block_ipv6_dns": false,
  "tun": {
    "name": "spaceship0",
    "mtu": 1500,
    "route_mode": "manual",
    "bypass_mark": 21328,
    "max_connections": 4096,
    "max_pending_connections": 1024,
    "dns_hijack": {
      "enabled": true,
      "query_timeout_seconds": 5,
      "tcp_idle_timeout_seconds": 10,
      "max_in_flight": 256,
      "max_in_flight_per_connection": 192
    }
  }
}

Zero-valued limits select the values shown above. dns_hijack.max_in_flight has a hard maximum of 1024 because every active UDP DNS flow owns a receive buffer of up to 64 KiB for its idle lifetime; the ceiling bounds those buffers to about 64 MiB. bypass_mark also defaults to decimal 21328 (0x5350). With TUN enabled, an omitted or zero mux selects the minimum persistent gRPC pool needed for the configured TCP and DNS concurrency; the defaults above require two connections. An explicit smaller value is rejected during config validation. A non-zero mux is a warm minimum: the shared pool grows when all current connections reach Spaceship's native per-connection stream limit, up to 255 persistent connections. This lets TUN, SOCKS, HTTP, REDIRECT, and DNS share capacity without silently overloading the initial TUN-sized pool. Outside TUN mode, mux: 0 retains the legacy unpooled behavior.

The growth threshold matches the native Spaceship server's HTTP/2 limit. If an intermediary advertises a lower concurrent-stream limit, configure enough initial mux connections to cover the expected peak at that lower value; otherwise the intermediary can queue streams before the local pool reaches its growth threshold. Server-wide Proxy admission still applies across every connection, so adding transports cannot bypass the server resource boundary. Spaceship applies that mark with SO_MARK to its gRPC control connection, direct TCP/UDP egress, forward-proxy connection, and pure-Go resolver sockets. The policy-routing rules must exempt that mark from the TUN or the tunnel will recursively capture its own control traffic.

route_mode currently accepts only manual. Spaceship creates the named IFF_TUN|IFF_NO_PI interface, sets its MTU, and brings it up, but deliberately does not change host addresses, routes, or policy rules. This keeps a bad config from replacing a production host's default route. One local-host pattern is:

# Run after Spaceship has created spaceship0.
ip addr add 198.18.0.1/30 dev spaceship0
ip route add default dev spaceship0 table 100

# Spaceship egress must use the ordinary routing table.
ip rule add pref 100 fwmark 0x5350/0xffffffff lookup main

# Example: capture other unmarked, non-local IPv4 traffic.
# The kernel's priority-0 local-table rule remains ahead of this rule.
ip rule add pref 110 not fwmark 0x5350/0xffffffff lookup 100

Treat that as a starting point, not a copy-paste policy for every host. Prefer scoping the capture rule to an application UID, cgroup-applied mark, source subnet, or dedicated network namespace. Add explicit higher-priority rules for the Spaceship server IPs and management networks as defense in depth. If IPv6 is required, assign an appropriate IPv6 address and install equivalent ip -6 route and ip -6 rule entries. Remove the capture rule before stopping Spaceship; otherwise new connections will be black-holed by a route whose TUN reader no longer exists.

Creating the interface and setting SO_MARK require Linux network administration capability (normally CAP_NET_ADMIN) and access to /dev/net/tun. The process also needs enough file descriptors for the configured connection limit. An externally provisioned descriptor may be passed as tun.file_descriptor; Spaceship duplicates it, validates that it is a single-queue IFF_TUN|IFF_NO_PI device without a virtio-net header, uses the interface's actual MTU, and closes only its duplicate. The descriptor's shared file status is made nonblocking as required by the gVisor endpoint.

With dns_hijack.enabled, every TCP or UDP flow whose original destination port is 53 is intercepted before normal route selection. Spaceship preserves the DNS wire message, including flags, response codes, EDNS, DNSSEC records, and authority/additional sections, and sends it over the authenticated gRPC connection. The server queries only its configured dns resolver (or the server's existing default of 8.8.8.8:53). An RPC or upstream failure becomes DNS SERVFAIL; the client never falls back to the original destination or a local resolver. DNS-over-TCP length framing, multiple queries per connection, and bounded pipelining are supported.

Pipelining is bounded twice. dns_hijack.max_in_flight caps concurrent DNS RPCs across all clients, and max_in_flight_per_connection caps those held by any single DNS-over-TCP connection. Without the second bound, one client that pipelines aggressively holds every slot and every other client behind the TUN receives SERVFAIL until its queries drain.

Zero sets the per-connection ceiling to roughly three quarters of max_in_flight — 192 of the default 256, far more headroom than a stub resolver pipelines — and leaves pools of four or fewer unrestricted, where capping would cost more pipelining than it buys. It may not exceed max_in_flight.

The ceiling is an upper bound, not the whole story: Spaceship also divides the pool by the number of DNS-over-TCP connections currently using it, so each is held to whichever is smaller. Two connections get half the pool each, four get a quarter each, and no connection is squeezed to nothing while there are no more connections than slots. Sharing bounds what a connection may acquire rather than revoking what it holds, so a connection that arrives while the pool is busy gains its share as in-flight queries drain — bounded by query_timeout_seconds — instead of instantly.

Reaching the per-connection ceiling applies backpressure instead of failing. Spaceship stops reading that socket until one of the connection's own queries completes, letting TCP flow control slow the client, so a deep pipeline is delayed rather than answered SERVFAIL while the shared pool still has room. Exhausting the global bound still returns SERVFAIL, because waiting there would stall one client on another's work. UDP needs no equivalent: each UDP flow answers one query at a time.

The server applies non-blocking global and per-user concurrency and token-bucket rate limits before starting an upstream exchange. The following shows the built-in defaults explicitly:

{
  "role": "server",
  "users": [{"uuid": "00000000-0000-0000-0000-000000000001"}],
  "dns_exchange": {
    "max_concurrent": 1024,
    "max_concurrent_per_user": 256,
    "queries_per_second": 4096,
    "queries_per_second_per_user": 1024,
    "burst": 1024,
    "burst_per_user": 256
  },
  "proxy_sessions": {
    "max_concurrent": 8192,
    "max_concurrent_per_user": 4096,
    "new_sessions_per_second": 8192,
    "new_sessions_per_second_per_user": 4096,
    "burst": 8192,
    "burst_per_user": 4096,
    "handshake_timeout_seconds": 10
  }
}

Zero selects the shown default for each field. When a rate is explicitly lowered and its burst remains zero, the implicit burst is capped at that rate; an explicitly configured larger burst is preserved. A per-user value cannot exceed its global counterpart. DNS admission covers both the wire exchange and the legacy record-oriented RPC; legacy batches are limited to 16 items. Saturated wire RPCs return gRPC ResourceExhausted, which the local DNS and TUN frontends convert to DNS SERVFAIL without fallback. Proxy admission is enforced across all HTTP/2 connections, and an authenticated stream that does not send its first routing header within the configured timeout is closed. This avoids hidden queues and idle-stream resource retention.

Monotonic request, forwarding, upstream-failure, timeout, and rejection counters are exposed in the dns_exchange and proxy_sessions objects returned by the loopback management /api/stats endpoint.

For a rolling upgrade, deploy servers with the wire DNS RPC before enabling dns_hijack on clients. A new client connected to an older server receives gRPC Unimplemented, returns DNS SERVFAIL, and deliberately does not bypass the tunnel through a local or destination resolver.

All non-DNS UDP traffic remains unsupported and is rejected by the netstack with an ICMP unreachable, so a QUIC client falls back to TCP immediately instead of stalling. Note that this applies to DNS too when dns_hijack is disabled: the netstack then registers no UDP protocol at all, and DNS sent through the TUN fails. Either enable dns_hijack or exempt port 53 from the capture rule so those queries never enter the TUN. Spaceship logs a warning at startup when TUN is enabled without DNS hijacking. Because TUN destinations are IP literals, Spaceship route matching has the same constraint as transparent REDIRECT: CIDR, exact-IP, and default rules apply, while domain rules cannot infer a hostname.

The opt-in Linux integration test creates an isolated network namespace and checks the real TUN device, kernel route, gVisor TCP handshake/stream, and TCP/UDP DNS interception:

SPACESHIP_TUN_INTEGRATION=1 \
go test -count=1 -run '^TestKernelTUNIntegration$' ./internal/tun

Nginx Reserve Proxy Configuration

...
    location /proxy. {
        grpc_intercept_errors on;
        grpc_socket_keepalive on;
        grpc_send_timeout 3600s;
        grpc_read_timeout 3600s;
        grpc_pass grpc://127.0.0.1:12345;
    }
...

Note that proxy is the current proto source package name

Safety

Spaceship currently uses pure gRPC with the insecure option. For secure communication, it is highly recommended to set up a reverse proxy with TLS, such as Nginx + TLS.

Development Status

The program is still under development. Contributions via pull requests are greatly appreciated.

This program is provided "as is," with no warranties or guarantees. It is available only to repository members, and sharing it with others is strictly prohibited. Users must adhere to the laws of their respective countries. Any illegal use of this program is strictly prohibited.

Directories

Path Synopsis
cmd
spaceship command
internal
dns
e2e
Package e2e holds full-stack tests that drive spaceship's front ends over the wire — a real SOCKS5 client against a real SOCKS5 listener — rather than calling transports directly.
Package e2e holds full-stack tests that drive spaceship's front ends over the wire — a real SOCKS5 client against a real SOCKS5 listener — rather than calling transports directly.
management
Package management provides a lightweight HTTP management API for the spaceship proxy.
Package management provides a lightweight HTTP management API for the spaceship proxy.
tun
pkg
dns
scripts
e2e command
Command e2e drives spaceship end to end as real processes: a server binary and a client binary talking over a real gRPC tunnel, exercised through the front ends an operator actually uses.
Command e2e drives spaceship end to end as real processes: a server binary and a client binary talking over a real gRPC tunnel, exercised through the front ends an operator actually uses.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL