Skip to content

Commit b15ea1b

Browse files
committed
major refractor
* removed option to run one proxy per port * added -subnet-size option to select one IP per network if provided (fixes #3) * use permute package to reduce memory overhead * use goreleaser to build packages automatically * major code refractor and modernizations * added -test flag to ensure routing is setup corectly * added fail-safe checks to pervent IP leaks * modernized the code
1 parent 6afca98 commit b15ea1b

16 files changed

+897
-453
lines changed

Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ default: stargate
33
RELEASE_DEPS = fmt lint
44
include release.mk
55

6+
SOURCES := $(shell find . -type f -name "*.go")
7+
8+
69
.PHONY: all fmt clean docker lint
710

8-
stargate: *.go go.mod
11+
stargate: ${SOURCES} go.mod go.sum
912
CGO_ENABLED=0 go build -ldflags "-w -s -X main.version=${VERSION}" -trimpath -o $@
1013

1114
clean:

README.md

Lines changed: 82 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Stargate
22

3-
Stargate runs TCP SOCKS proxies on different ports egressing on sequential IPs in the same subnet.
3+
Stargate is a TCP SOCKS5 proxy server that can egress traffic from multiple IP addresses within a subnet. It randomly distributes connections across different IP addresses to help avoid rate-limiting and provide load balancing across your available IP range.
4+
45
This requires the host running stargate to have the subnet routed directly to it.
56

67
If you have an IPv6 subnet, stargate can allow you to make full use of it by any program that can speak SOCKS.
@@ -9,62 +10,117 @@ If you have an IPv6 subnet, stargate can allow you to make full use of it by any
910

1011
```console
1112
Usage of ./stargate: [OPTION]... CIDR
12-
CIDR example: "192.0.2.0/24"
13+
CIDR example: "192.0.2.0/24"
1314
OPTIONS:
1415
-listen string
15-
IP to listen on (default "localhost")
16-
-port uint
17-
first port to start listening on
18-
-random uint
19-
port to use for random proxy server
16+
listen on specified [IP:]port (e.g., '1337', '127.0.0.1:8080', '[::1]:1080') (default "localhost:1080")
2017
-subnet-size uint
21-
CIDR prefix length for random subnet proxy (e.g., 24 for /24 subnets)
18+
CIDR prefix length for random subnet proxy (e.g., 64 for /64 IPv6 subnets)
19+
-test
20+
run test request on all IPs and exit
2221
-verbose
23-
enable verbose logging
22+
enable verbose logging
2423
-version
25-
print version and exit
24+
print version and exit
2625
```
2726

28-
## Random
27+
Stargate now operates as a single SOCKS5 proxy server that randomly selects egress IP addresses from your specified CIDR range. This approach is much more memory-efficient and suitable for large IPv6 ranges.
2928

30-
The `-random` flag starts a SOCKS5 proxy that egresses traffic on a random IP in the subnet.
31-
This is useful to avoid rate-limiting or in situations where there are too many IPs in the subnet to listen on each port which is common with IPv6.
29+
## Test Flag - Preventing IP Address Leakage
3230

33-
When used with `-subnet-size`, the proxy will randomly distribute connections across different subnets within the main CIDR range. For example, with a /48 IPv6 block and `-subnet-size 64`, connections will be distributed across random /64 subnets.
31+
**IMPORTANT**: Before using Stargate in production, always run the test mode first to ensure there are no unintended IP address leaks.
3432

35-
## Example
33+
The `-test` flag performs comprehensive validation by:
3634

37-
The following will start 254 SOCKS proxies listening on 127.0.0.7 ports 10001-100254 sending traffic egressing on 192.0.2.1 through 192.0.2.254.
35+
- Testing HTTP requests from every available IP address in your CIDR range
36+
- Verifying that each egress IP matches the intended source address
37+
- Detecting binding errors or network misconfigurations
38+
- Ensuring no connections leak through unintended IP addresses
3839

39-
```console
40-
./stargate -listen 127.0.0.7 -port 10001 192.0.2.0/24
40+
**Note:** When using `-subnet-size`, the test will validate one randomly selected IP address from each subnet rather than testing every possible IP. For example, with `-subnet-size 64` on a /48, it tests one IP per /64 subnet, not every IP in the entire /48. In order to test every possible IP address, do not pass a `-subnet-size` option when using `-test`.
41+
42+
**Always run this test before production use:**
43+
44+
```bash
45+
# Test your configuration first - THIS IS CRITICAL!
46+
./stargate -test 192.0.2.0/24
47+
48+
# Only proceed to normal operation after tests pass
49+
./stargate 192.0.2.0/24
4150
```
4251

43-
The following will start a single socks proxy listening on 127.0.0.1:1337 egressing each connection from a random IP in 2001:DB8:1337::1/64 This offers you 2<sup>64</sup> possible IPs to egress on.
52+
The test will fail immediately if any IP address binding issues are detected, preventing potential IP leakage that could compromise your setup.
4453

45-
```console
46-
./stargate -random 1337 2001:DB8:1337::1/64
54+
## Subnet Distribution
55+
56+
When used with `-subnet-size`, the proxy will randomly distribute connections across different subnets within the main CIDR range. For example, with a /48 IPv6 block and `-subnet-size 64`, connections will be distributed across random /64 subnets, giving you access to multiple /64 networks within your larger allocation.
57+
58+
## Examples
59+
60+
### Basic Usage
61+
62+
Start a SOCKS5 proxy on the default port (1080) that randomly egresses from IPs in the 192.0.2.0/24 range:
63+
64+
```bash
65+
# Always test first!
66+
./stargate -test 192.0.2.0/24
67+
68+
# Run the proxy after tests pass
69+
./stargate 192.0.2.0/24
4770
```
4871

49-
The following will start a single socks proxy listening on 127.0.0.1:8080 that distributes connections across random /64 subnets within a /48 IPv6 block:
72+
### Custom Listen Address
5073

51-
```console
52-
./stargate -random 8080 -subnet-size 64 2001:DB8:1337::/48
74+
Start a SOCKS5 proxy listening on a specific IP and port:
75+
76+
```bash
77+
./stargate -listen 127.0.0.7:8080 192.0.2.0/24
5378
```
5479

80+
### IPv6 with Large Address Space
81+
82+
Use an IPv6 /64 subnet - this gives you 2^64 possible egress IPs:
83+
84+
```bash
85+
./stargate -test 2001:DB8:1337::/64
86+
./stargate 2001:DB8:1337::/64
87+
```
88+
89+
### Subnet-Level Distribution
90+
91+
Distribute connections across multiple /64 subnets within a /48 IPv6 allocation:
92+
93+
```bash
94+
./stargate -test -subnet-size 64 2001:DB8:1337::/48
95+
./stargate -subnet-size 64 2001:DB8:1337::/48
96+
```
97+
98+
This will randomly select from different /64 networks within your /48, providing both IP and subnet-level distribution.
99+
55100
## Download
56101

57102
### [Precompiled Binaries](https://github.com/lanrat/stargate/releases)
58103

104+
The easiest way to get started is with [precompiled binaries](https://github.com/lanrat/stargate/releases) available for multiple platforms including Linux and FreeBSD. These are statically linked and ready to run without additional dependencies.
59105

60106
### [Docker](https://github.com/lanrat/stargate/pkgs/container/stargate)
61107

62-
Stargate can be run inside Docker as well, but it will require fancy routing rules or `--net=host`.
108+
Docker is particularly useful for deployment in containerized environments, though network configuration requires special attention to ensure proper subnet routing.
109+
110+
Running in docker will require `--net=host`, or the subnet must be routed directly to the container.
63111

64112
```shell
65113
docker pull ghcr.io/lanrat/stargate:latest
66114
```
67115

68116
## Building
69117

70-
Just run `make`!
118+
Building from source is straightforward - just run `make`!
119+
120+
```bash
121+
git clone https://github.com/lanrat/stargate.git
122+
cd stargate
123+
make
124+
```
125+
126+
This will produce a statically linked binary that's ready to use.

addresses.go

Lines changed: 61 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -2,71 +2,80 @@ package main
22

33
import (
44
"fmt"
5-
"math"
6-
"math/big"
7-
"math/rand"
85
"net"
96
"net/netip"
107
)
118

12-
// TODO possible enhancement
13-
// dial from iface: https://gist.github.com/creack/43ee6542ddc6fe0da8c02bd723d5cc53
9+
// broadcastAddrs is a global map that tracks IP addresses identified as broadcast addresses.
10+
// It is populated by checkHostConflicts and used to prevent binding to these addresses.
11+
var broadcastAddrs = make(map[string]bool)
1412

15-
// hosts returns a slice of all usable host IP addresses within the given CIDR range.
16-
// It excludes the network address and broadcast address for IPv4 networks.
17-
// Uses the modern netip package for cleaner iteration.
18-
// Updated based on: https://gist.github.com/kotakanbe/d3059af990252ba89a82?permalink_comment_id=4105265#gistcomment-4105265
19-
func hosts(cidr *net.IPNet) ([]net.IP, error) {
20-
// Convert to netip.Prefix for cleaner iteration
21-
ip, ok := netip.AddrFromSlice(cidr.IP)
22-
if !ok {
23-
return nil, fmt.Errorf("invalid IP address")
24-
}
25-
ones, _ := cidr.Mask.Size()
26-
prefix := netip.PrefixFrom(ip.Unmap(), ones)
27-
28-
var ips []net.IP
29-
for addr := prefix.Addr(); prefix.Contains(addr); addr = addr.Next() {
30-
// Convert netip.Addr back to net.IP
31-
ips = append(ips, net.IP(addr.AsSlice()))
13+
// checkHostConflicts detects if any of the addresses we are going to use are broadcast addresses.
14+
// It populates the global broadcastAddrs map by examining all system network interfaces.
15+
func checkHostConflicts(prefix *netip.Prefix) error {
16+
interfaces, err := net.Interfaces()
17+
if err != nil {
18+
return err
3219
}
3320

34-
// For single host or empty range, return as-is
35-
if len(ips) < 2 {
36-
return ips, nil
21+
for _, i := range interfaces {
22+
addrs, err := i.Addrs()
23+
if err != nil {
24+
return err
25+
}
26+
for _, a := range addrs {
27+
ipnet, ok := a.(*net.IPNet)
28+
if !ok {
29+
continue
30+
}
31+
// We are only interested in IPv4 addresses for broadcast.
32+
if ipnet.IP.To4() == nil {
33+
continue
34+
}
35+
brdIP, err := getBroadcastAddressFromAddr(ipnet)
36+
if err != nil {
37+
return err
38+
}
39+
brdAddr, ok := netip.AddrFromSlice(brdIP)
40+
if !ok {
41+
return fmt.Errorf("unable to parse IP to addr: %+v", brdAddr)
42+
}
43+
if prefix.Contains(brdAddr) {
44+
broadcastAddrs[brdAddr.String()] = true
45+
l.Printf("WARNING: interface %s broadcast address is within provided prefix %s", i.Name, brdIP)
46+
}
47+
}
3748
}
3849

39-
// Remove network and broadcast addresses (first and last)
40-
// This handles both IPv4 and IPv6 appropriately
41-
return ips[1 : len(ips)-1], nil
50+
return nil
4251
}
4352

44-
// maskSize returns the number of addresses in the network mask as a big.Int,
45-
// which can handle arbitrarily large address spaces (e.g., IPv6).
46-
func maskSize(m *net.IPMask) big.Int {
47-
var size big.Int
48-
maskBits, totalBits := m.Size()
49-
addrBits := totalBits - maskBits
50-
size.Lsh(big.NewInt(1), uint(addrBits))
51-
return size
52-
}
53+
// getBroadcastAddressFromAddr calculates the broadcast address from a net.IPNet.
54+
// It only supports IPv4 addresses and returns an error for IPv6 or invalid inputs.
55+
func getBroadcastAddressFromAddr(addr net.Addr) (net.IP, error) {
56+
// Type assertion to check if the net.Addr is a *net.IPNet.
57+
ipnet, ok := addr.(*net.IPNet)
58+
if !ok {
59+
return nil, fmt.Errorf("address is not a net.IPNet type: %T", addr)
60+
}
5361

54-
// randomIP generates a random IP address within the given CIDR range.
55-
// It preserves the network portion and randomizes the host portion.
56-
func randomIP(cidr *net.IPNet) net.IP {
57-
ip := cidr.IP
58-
for i := range ip {
59-
rb := byte(rand.Intn(math.MaxUint8))
60-
ip[i] = (cidr.Mask[i] & ip[i]) + (^cidr.Mask[i] & rb)
62+
// Check if the IP is an IPv4 address.
63+
if ipnet.IP.To4() == nil {
64+
return nil, fmt.Errorf("only IPv4 addresses are supported for broadcast calculation")
6165
}
62-
return ip
63-
}
6466

65-
// getIPNetwork returns "ip4" for IPv4 addresses or "ip6" for IPv6 addresses.
66-
// This is used for DNS resolution context.
67-
func getIPNetwork(ip *net.IP) string {
68-
if ip.To4() != nil {
69-
return "ip4"
67+
// Perform the bitwise OR calculation.
68+
// Use IPv4 representation to avoid length mismatches
69+
ip4 := ipnet.IP.To4()
70+
mask4 := ipnet.Mask
71+
if len(mask4) != 4 {
72+
return nil, fmt.Errorf("invalid IPv4 mask length: %d", len(mask4))
7073
}
71-
return "ip6"
74+
75+
broadcast := make(net.IP, 4)
76+
for i := 0; i < 4; i++ {
77+
broadcast[i] = ip4[i] | ^mask4[i]
78+
}
79+
80+
return broadcast, nil
7281
}

0 commit comments

Comments
 (0)