# ClientConnector Refactor Part 3: Multiple Chains of Different Lengths

**Status: IMPLEMENTED**

## Problem Statement

Currently, `RuleActionConfig::Allow` supports a single `client_chain`:

```rust
RuleActionConfig::Allow {
    override_address: Option<NetLocation>,
    client_chain: OneOrSome<ClientChainHop>,
}
```

This allows per-hop pooling (round-robin between connectors at each hop position), but **all connections use the same chain structure** with the same number of hops.

Users want to round-robin between **entire chains of different lengths**:

```
                    ┌─ direct ──────────────────────────────────┐
                    │                                           │
Request ─── RR ─────┼─ proxy1 → proxy2 → proxy3 ───────────────┼──→ Target
                    │                                           │
                    └─ proxy4 → proxy5 ────────────────────────┘
```

This is useful for:
- Load balancing between direct and proxied paths
- Having backup chains of different configurations
- Geographic routing with different hop counts

## Design Goals

1. **Backward compatible**: Existing `client_chain` syntax continues to work unchanged
2. **Clean YAML syntax**: Avoid awkward `- -` nested array syntax
3. **Extensible**: Support future per-chain options (weights, names)
4. **Type-safe**: Prevent invalid configurations at compile time where possible
5. **Clear error messages**: Helpful validation errors for empty chains

## Config Type Changes

### New Type: `ClientChain`

A complete proxy chain (sequence of hops):

```rust
/// A complete proxy chain - a sequence of hops that traffic traverses.
#[derive(Debug, Clone)]
pub struct ClientChain {
    /// The hops in this chain, from first (closest to us) to last (closest to target).
    pub hops: OneOrSome<ClientChainHop>,
    // Future extensibility: weight, name, conditions, etc.
}
```

### Updated `RuleActionConfig`

```rust
RuleActionConfig::Allow {
    override_address: Option<NetLocation>,
    /// Multiple chains for round-robin selection.
    /// Field name: `client_chains` (alias: `client_chain` for backward compatibility)
    client_chains: NoneOrSome<ClientChain>,
}
```

Where:
- `NoneOrSome::Unspecified` → Default to one chain with single direct hop
- `NoneOrSome::None` → Error: explicitly empty is invalid
- `NoneOrSome::One(chain)` → Single chain (backward compatible)
- `NoneOrSome::Some(chains)` → Multiple chains for round-robin

## YAML Syntax

### Backward Compatible (Single Chain)

Existing configs work unchanged:

```yaml
# Single chain with one hop
client_chain: my-proxy-group

# Single chain with multiple hops
client_chain:
  - entry-proxy
  - exit-proxy

# Single chain with pool at a hop
client_chain:
  - pool: [proxy-us, proxy-eu]
  - exit-proxy
```

### Multiple Chains - Object Syntax (Recommended)

Clear, explicit syntax using `chain:` key:

```yaml
client_chains:
  - chain: [direct]
  - chain: [proxy1, proxy2, proxy3]
  - chain: [proxy4, proxy5]
```

With per-hop pools:

```yaml
client_chains:
  - chain:
      - direct
  - chain:
      - pool: [entry-us, entry-eu]
      - middle-proxy
      - pool: [exit-asia, exit-europe]
  - chain:
      - fast-proxy
```

### Multiple Chains - Array Syntax (Compact)

For simple cases, bare arrays work too:

```yaml
client_chains:
  - [direct]
  - [proxy1, proxy2, proxy3]
  - [proxy4, proxy5]
```

**Note**: This produces `- -` in some YAML representations, which some find awkward:
```yaml
client_chains:
- - direct
- - proxy1
  - proxy2
  - proxy3
```

### Mixed Syntax

Both forms can be mixed:

```yaml
client_chains:
  - [direct]                              # Bare array
  - chain: [proxy1, proxy2, proxy3]       # Object with chain key
  - chain:
      - pool: [proxy-a, proxy-b]          # Complex chain with pools
      - exit-proxy
```

### Future Extensibility

The object syntax enables per-chain options later:

```yaml
client_chains:
  - chain: [direct]
    weight: 1
    name: direct-path
  - chain: [proxy1, proxy2]
    weight: 3
    name: proxied-path
```

## Deserialization Logic

`ClientChain` deserializes from:

1. **Array** `[hop1, hop2, ...]` → `ClientChain { hops: [...] }`
2. **Object with `chain` key** `{ chain: [...] }` → `ClientChain { hops: [...] }`
3. **Single hop** (string or inline config) → `ClientChain { hops: [hop] }`

```rust
impl<'de> Deserialize<'de> for ClientChain {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        struct ClientChainVisitor;

        impl<'de> Visitor<'de> for ClientChainVisitor {
            type Value = ClientChain;

            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
                f.write_str(
                    "a chain: either an array of hops, an object with 'chain' key, \
                     or a single hop (string/object)"
                )
            }

            // String → single hop chain
            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> {
                Ok(ClientChain {
                    hops: OneOrSome::One(ClientChainHop::Single(
                        ConfigSelection::GroupName(value.to_string())
                    )),
                })
            }

            // Array → sequence of hops
            fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error> {
                let hops: Vec<ClientChainHop> = /* deserialize sequence */;
                if hops.is_empty() {
                    return Err(Error::custom("chain cannot be empty"));
                }
                Ok(ClientChain {
                    hops: if hops.len() == 1 {
                        OneOrSome::One(hops.into_iter().next().unwrap())
                    } else {
                        OneOrSome::Some(hops)
                    },
                })
            }

            // Object → check for 'chain' key or treat as inline config
            fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error> {
                // If has 'chain' key → extract hops from it
                // Else → treat as single inline ClientConfig hop
            }
        }

        deserializer.deserialize_any(ClientChainVisitor)
    }
}
```

## Validation Rules

### At Deserialization Time

1. **Empty chain array `[]`**: Error - "chain cannot be empty"
2. **Empty `chain` in object `{ chain: [] }`**: Error - "chain cannot be empty"

### At Config Validation Time

1. **`client_chains` unspecified**: Default to `[[direct]]`
2. **`client_chains: []`**: Error - "client_chains cannot be empty; omit the field for default direct connection"
3. **Any chain empty after group expansion**: Error - "chain at index N is empty after expanding group references"
4. **Direct connector at hop > 0**: Error (existing validation)

## Runtime Changes

### New Struct: `ClientProxySelector`

Replaces direct use of `ClientProxyChain` for managing multiple chains:

```rust
/// Manages multiple proxy chains with round-robin selection.
///
/// For TCP: round-robin across all chains
/// For UDP: round-robin only across UDP-capable chains
pub struct ClientProxySelector {
    /// All available chains
    chains: Vec<ClientProxyChain>,
    /// Round-robin index for TCP connections
    next_tcp_index: AtomicU32,
    /// Indices of chains that support UDP (have UDP-capable final hop)
    udp_chain_indices: Vec<usize>,
    /// Round-robin index for UDP connections (indexes into udp_chain_indices)
    next_udp_index: AtomicU32,
}

impl ClientProxySelector {
    pub fn new(chains: Vec<ClientProxyChain>) -> Self {
        assert!(!chains.is_empty(), "Must have at least one chain");

        // Find which chains support UDP
        let udp_chain_indices: Vec<usize> = chains
            .iter()
            .enumerate()
            .filter(|(_, chain)| chain.supports_udp())
            .map(|(i, _)| i)
            .collect();

        Self {
            chains,
            next_tcp_index: AtomicU32::new(0),
            udp_chain_indices,
            next_udp_index: AtomicU32::new(0),
        }
    }

    /// Connect for TCP traffic - round-robin across all chains
    pub async fn connect(
        &self,
        remote_location: &NetLocation,
        resolver: &Arc<dyn Resolver>,
    ) -> std::io::Result<TcpClientSetupResult> {
        let idx = self.next_tcp_index.fetch_add(1, Ordering::Relaxed) as usize;
        let chain = &self.chains[idx % self.chains.len()];
        chain.connect(remote_location, resolver).await
    }

    /// Connect for UDP traffic - round-robin across UDP-capable chains only
    pub async fn connect_udp(
        &self,
        resolver: &Arc<dyn Resolver>,
        request: UdpStreamRequest,
    ) -> std::io::Result<TcpClientUdpSetupResult> {
        if self.udp_chain_indices.is_empty() {
            return Err(std::io::Error::new(
                std::io::ErrorKind::Unsupported,
                "No chains support UDP",
            ));
        }

        let rr_idx = self.next_udp_index.fetch_add(1, Ordering::Relaxed) as usize;
        let chain_idx = self.udp_chain_indices[rr_idx % self.udp_chain_indices.len()];
        self.chains[chain_idx].connect_udp(resolver, request).await
    }

    /// Check if any chain supports UDP
    pub fn supports_udp(&self) -> bool {
        !self.udp_chain_indices.is_empty()
    }
}
```

### Update `ClientProxyChain`

Add method to check if chain supports UDP:

```rust
impl ClientProxyChain {
    /// Returns true if this chain supports UDP connections.
    /// A chain supports UDP if its final hop has UDP-capable connectors.
    pub fn supports_udp(&self) -> bool {
        !self.udp_final_hop_indices.is_empty()
    }
}
```

## Examples

### Example 1: Direct with Proxy Fallback

Round-robin between direct connection and a proxy:

```yaml
rules:
  - masks: "0.0.0.0/0"
    action: allow
    client_chains:
      - chain: [direct]
      - chain:
          - address: "proxy.example.com:1080"
            protocol:
              type: socks
```

### Example 2: Geographic Load Balancing

Different paths for different regions:

```yaml
client_groups:
  us-entry:
    - address: "us1.example.com:443"
      # ...
    - address: "us2.example.com:443"
      # ...
  eu-entry:
    - address: "eu1.example.com:443"
      # ...
  asia-exit:
    - address: "asia.example.com:443"
      # ...

rules:
  - masks: "0.0.0.0/0"
    action: allow
    client_chains:
      # US path: 2 hops
      - chain:
          - us-entry
          - asia-exit
      # EU path: 2 hops
      - chain:
          - eu-entry
          - asia-exit
      # Direct path: 1 hop
      - chain: [direct]
```

### Example 3: Complex Multi-Hop with Pools

Per-hop load balancing within multi-chain selection:

```yaml
client_chains:
  # Chain 1: Simple direct
  - chain: [direct]

  # Chain 2: 3-hop with pools at each level
  - chain:
      - pool: [entry-us-1, entry-us-2, entry-eu-1]
      - pool: [middle-relay-a, middle-relay-b]
      - pool: [exit-asia, exit-europe]

  # Chain 3: 2-hop fast path
  - chain:
      - fast-entry
      - fast-exit
```

### Example 4: Backward Compatible Single Chain

All existing configs continue to work:

```yaml
# These are all equivalent to single-chain behavior:

# Just a group reference
client_chain: my-proxies

# Array of hops
client_chain:
  - entry-proxy
  - exit-proxy

# Explicit single chain in new syntax
client_chains:
  - chain: [entry-proxy, exit-proxy]
```

## Migration Path

1. **No breaking changes**: Existing `client_chain` field works unchanged
2. **Gradual adoption**: Users can start using `client_chains` when they need multiple chains
3. **Clear errors**: If both `client_chain` and `client_chains` are specified, error with clear message

## Implementation Summary

### Files Modified

1. **`src/config/types/rules.rs`**
   - Added `ClientChain` type with flexible deserialization (array, object with `chain:` key, single hop)
   - Updated `RuleActionConfig::Allow` to use `client_chains: NoneOrSome<ClientChain>` (aliased as `client_chain`)
   - Added 31 comprehensive unit tests for config parsing

2. **`src/config/validate.rs`**
   - Updated validation to handle multiple chains
   - Added validation for empty `client_chains: []` (error)
   - Added chain index to error messages for clarity

3. **`src/client_proxy_chain.rs`**
   - Added `supports_udp()` method to `ClientProxyChain`
   - Added `ClientChainGroup` struct for round-robin selection across multiple chains
   - Separate TCP and UDP round-robin (UDP only selects from UDP-capable chains)

4. **`src/client_proxy_selector.rs`**
   - Updated `ConnectAction` and `ConnectDecision` to use `ClientChainGroup`

5. **`src/tcp/tcp_handler_util.rs`**
   - Added `build_client_chain_group()` to create `ClientChainGroup` from config

6. **Server files** (`tcp_server.rs`, `quic_server.rs`, `hysteria2_server.rs`, `tuic_server.rs`)
   - Updated all usages from `chain` to `chain_group`

### Test Results

- All 31 new config tests pass
- All 35 client_proxy_chain tests pass
- All 18 validation tests pass
- Integration tests: 59 passed, 8 failed (pre-existing QUIC protocol issues)

## Open Questions (Resolved)

1. **Serialization format**: Uses object syntax `{ chain: [...] }` for clarity
2. **Naming**: Used `ClientChainGroup` to avoid confusion with existing `ClientProxySelector`
3. **Weight support**: Deferred until needed
