# How to Implement Upstream SOCKS5 Chaining on the MasterDnsVPN Server Side

> Learn to implement upstream SOCKS5 chaining on the MasterDnsVPN server side. Configure proxy hops and modify dial logic for nested SOCKS5 CONNECT requests.

- Repository: [Amin Mahmoudi/MasterDnsVPN](https://github.com/masterking32/MasterDnsVPN)
- Tags: how-to-guide
- Published: 2026-05-10

---

**To implement upstream SOCKS5 chaining in MasterDnsVPN, extend the server configuration to accept a slice of proxy hops, store the ordered list in the Server struct, and modify the dial logic to nest SOCKS5 CONNECT requests recursively from the last hop back to the first.**

MasterDnsVPN currently supports routing outbound traffic through a single external SOCKS5 proxy via the `dialExternalSOCKS5TargetContext` function in [`internal/udpserver/socks5_upstream.go`](https://github.com/masterking32/MasterDnsVPN/blob/main/internal/udpserver/socks5_upstream.go). Enabling chaining—routing requests through a series of proxies before reaching the final destination—requires changes to the configuration schema, server state management, and connection establishment logic.

## Extend the Configuration Model

The existing server configuration in [`internal/config/server_config.go`](https://github.com/masterking32/MasterDnsVPN/blob/main/internal/config/server_config.go) defines isolated fields for a single proxy (`UseExternalSOCKS5`, `ForwardIP`, `ForwardPort`). Replace these with a slice of `Socks5Upstream` structs to support arbitrary hop counts:

```go
type Socks5Upstream struct {
    Host     string `toml:"host"`      // IP or hostname of the proxy
    Port     uint16 `toml:"port"`      // Proxy port
    Auth     bool   `toml:"auth"`      // Whether USER/PASS auth is required
    Username string `toml:"username"`  // Optional username
    Password string `toml:"password"`  // Optional password
}

type ServerConfig struct {
    // … existing fields …
    Socks5Chain []Socks5Upstream `toml:"socks5_chain"` // Ordered list of upstream proxies
}

```

This structure allows you to define multiple hops in your TOML configuration:

```toml
[[socks5_chain]]
host = "10.0.0.1"
port = 1080
auth = false

[[socks5_chain]]
host = "10.0.0.2"
port = 1080
auth = true
username = "proxyuser"
password = "proxypass"

```

## Store the Chain in Server State

The `Server` type in [`internal/udpserver/server.go`](https://github.com/masterking32/MasterDnsVPN/blob/main/internal/udpserver/server.go) manages runtime state. Add a field to hold the ordered proxy list and expose a helper method to populate it during initialization:

```go
type Server struct {
    // … existing fields …
    socks5Chain []Socks5Upstream // Ordered list of proxies
}

func (s *Server) SetSocks5Chain(chain []internalconfig.Socks5Upstream) {
    s.socks5Chain = chain
}

```

In [`cmd/server/main.go`](https://github.com/masterking32/MasterDnsVPN/blob/main/cmd/server/main.go), pass the configuration slice when constructing the server instance:

```go
srv := UDPServer.New(cfg, log, codec)
srv.SetSocks5Chain(cfg.Socks5Chain)

```

## Implement the Chaining Dialer

Replace the single-hop `dialExternalSOCKS5TargetContext` with a chain-aware implementation in [`internal/udpserver/socks5_upstream.go`](https://github.com/masterking32/MasterDnsVPN/blob/main/internal/udpserver/socks5_upstream.go). The logic builds nested payloads from last hop to first, then performs sequential handshakes:

```go
// dialChainedSOCKS5TargetContext connects through the configured SOCKS5 chain.
func (s *Server) dialChainedSOCKS5TargetContext(ctx context.Context, targetPayload []byte) (net.Conn, error) {
    // Build the hop list: if a chain is defined, use it; otherwise fall back to the legacy single proxy.
    hops := s.socks5Chain
    if len(hops) == 0 && s.useExternalSOCKS5 {
        hops = []Socks5Upstream{{Host: s.externalSOCKS5Address, Port: s.externalSOCKS5Port, Auth: s.externalSOCKS5Auth, Username: s.externalSOCKS5User, Password: s.externalSOCKS5Pass}}
    }
    if len(hops) == 0 {
        // No upstream configured → direct TCP connection.
        return s.dialTCPTargetContext(ctx, net.JoinHostPort(hostFromPayload(targetPayload), strconv.Itoa(int(portFromPayload(targetPayload)))))
    }

    // Start from the *last* hop and work backwards, nesting the payload.
    payload := targetPayload
    for i := len(hops) - 1; i >= 0; i-- {
        hop := hops[i]
        // Build a CONNECT request that points to the *previous* hop (or final destination for the innermost hop).
        payload = buildSocks5ConnectPayload(hop.Host, hop.Port, payload)
    }

    // Open TCP connection to the *first* hop.
    first := hops[0]
    conn, err := s.dialTCPTargetContext(ctx, net.JoinHostPort(first.Host, strconv.Itoa(int(first.Port))))
    if err != nil {
        return nil, err
    }

    // Perform the handshake for each hop in order.
    for _, hop := range hops {
        if err = s.performSocks5Handshake(ctx, conn, hop); err != nil {
            conn.Close()
            return nil, err
        }
    }
    return conn, nil
}

```

Support this with helper functions to construct nested payloads and handle per-hop authentication:

```go
// buildSocks5ConnectPayload creates a SOCKS5 CONNECT request wrapping the inner payload.
func buildSocks5ConnectPayload(host string, port uint16, inner []byte) []byte {
    var addr []byte
    ip := net.ParseIP(host)
    if ip4 := ip.To4(); ip4 != nil {
        addr = append([]byte{0x01}, ip4...)
    } else if ip != nil {
        addr = append([]byte{0x04}, ip...)
    } else {
        addr = append([]byte{0x03, byte(len(host))}, []byte(host)...)
    }
    portBytes := []byte{byte(port >> 8), byte(port & 0xff)}
    header := []byte{0x05, 0x01, 0x00}
    return append(append(append(header, addr...), portBytes...), inner...)
}

// performSocks5Handshake runs greeting, auth, and CONNECT for a single hop.
func (s *Server) performSocks5Handshake(ctx context.Context, conn net.Conn, hop Socks5Upstream) error {
    // Greeting
    greeting := []byte{0x05, 0x01, 0x00}
    if hop.Auth {
        greeting[2] = 0x02
    }
    if err := writeAll(conn, greeting); err != nil {
        return err
    }
    
    // Receive method selection
    var methodResp [2]byte
    if _, err := io.ReadFull(conn, methodResp[:]); err != nil {
        return err
    }
    if methodResp[0] != 0x05 {
        return fmt.Errorf("unexpected SOCKS5 version %d", methodResp[0])
    }
    
    // USER/PASS auth if required
    if hop.Auth {
        if err := s.handleExternalSOCKS5Auth(conn, methodResp[1]); err != nil {
            return err
        }
    }
    
    // Read CONNECT response
    var respHeader [4]byte
    if _, err := io.ReadFull(conn, respHeader[:]); err != nil {
        return err
    }
    if respHeader[1] != 0x00 {
        return fmt.Errorf("SOCKS5 hop %s:%d failed, code %d", hop.Host, hop.Port, respHeader[1])
    }
    return discardSOCKS5BoundAddress(conn, respHeader[3])
}

```

## Integrate the Chain into Request Handling

Update `dialSOCKSStreamTargetContext` to detect when a chain is configured and delegate to the new chaining logic:

```go
func (s *Server) dialSOCKSStreamTargetContext(ctx context.Context, host string, port uint16, targetPayload []byte) (net.Conn, error) {
    if err := validateSOCKSTargetHost(host); err != nil {
        return nil, err
    }
    // If a SOCKS5 chain is defined, use it.
    if len(s.socks5Chain) > 0 {
        finalPayload := buildSocks5ConnectPayload(host, port, nil)
        return s.dialChainedSOCKS5TargetContext(ctx, finalPayload)
    }
    // Existing fallback: direct TCP or single upstream.
    if !s.useExternalSOCKS5 || len(targetPayload) == 0 {
        return s.dialTCPTargetContext(ctx, net.JoinHostPort(host, strconv.Itoa(int(port))))
    }
    return s.dialExternalSOCKS5TargetContext(ctx, targetPayload)
}

```

## Testing and Validation

Validate your implementation using two approaches:

- **Unit tests**: Create [`internal/udpserver/socks5_chain_test.go`](https://github.com/masterking32/MasterDnsVPN/blob/main/internal/udpserver/socks5_chain_test.go) to mock multiple SOCKS5 servers using the `net` package. Verify that handshakes propagate through each hop and that the final payload reaches the intended destination.
- **Integration tests**: Update [`scripts/bench/README.md`](https://github.com/masterking32/MasterDnsVPN/blob/main/scripts/bench/README.md) with a chain mode benchmark. Configure a local TOML file with two or more test proxies and confirm that traffic flows sequentially through each hop before exiting to the target.

## Summary

- **Configuration**: Replace single-proxy fields with a `Socks5Chain` slice in [`internal/config/server_config.go`](https://github.com/masterking32/MasterDnsVPN/blob/main/internal/config/server_config.go) to define multiple hops with independent authentication.
- **State management**: Store the ordered proxy list in the `Server` struct via `SetSocks5Chain` in [`internal/udpserver/server.go`](https://github.com/masterking32/MasterDnsVPN/blob/main/internal/udpserver/server.go).
- **Dial logic**: Implement `dialChainedSOCKS5TargetContext` in [`internal/udpserver/socks5_upstream.go`](https://github.com/masterking32/MasterDnsVPN/blob/main/internal/udpserver/socks5_upstream.go) to build nested SOCKS5 payloads from last hop to first, then perform sequential handshakes.
- **Request routing**: Modify `dialSOCKSStreamTargetContext` to prioritize the chain when `socks5Chain` has entries, falling back to single-proxy or direct TCP mode otherwise.
- **Error handling**: Any failure during the chain aborts the connection and returns a descriptive error, ensuring failed hops do not leak connections.

## Frequently Asked Questions

### What is upstream SOCKS5 chaining and why is it useful?

Upstream SOCKS5 chaining routes client traffic through multiple intermediary SOCKS5 proxies before reaching the final destination. This enables complex routing scenarios such as multi-hop anonymity layers, geographic routing through specific jurisdictions, or load balancing across different upstream providers. According to the MasterDnsVPN source code, this extends the existing single-proxy capability in [`socks5_upstream.go`](https://github.com/masterking32/MasterDnsVPN/blob/main/socks5_upstream.go) to support arbitrary hop depths.

### How does authentication work when chaining multiple SOCKS5 proxies?

Each hop in the chain maintains its own authentication credentials via the `Socks5Upstream` struct fields (`Auth`, `Username`, `Password`). During `performSocks5Handshake`, the server checks the current hop's authentication flag and invokes `handleExternalSOCKS5Auth` only when method `0x02` (USER/PASS) is required, allowing you to mix authenticated and non-authenticated proxies within the same chain.

### What happens if one proxy in the chain is unreachable?

If any hop in the sequence fails—whether during the initial TCP connection in `dialTCPTargetContext` or during the SOCKS5 handshake in `performSocks5Handshake`—the function immediately closes the connection and returns an error. This "fail fast" behavior prevents partial tunnels and ensures traffic does not leak through an incomplete chain.

### Can I combine SOCKS5 chaining with the existing single-proxy configuration?

Yes. The implementation in `dialChainedSOCKS5TargetContext` checks `len(s.socks5Chain)` first. If the slice is empty but `s.useExternalSOCKS5` is true, it falls back to the legacy single-hop behavior using `s.externalSOCKS5Address`. If both are unset, it establishes a direct TCP connection, maintaining backward compatibility with existing configurations.