ginuerzh/gost · error

empty chain

Error message

empty chain

What it means

ErrEmptyChain is a sentinel error indicating the proxy Chain has no node groups (it is empty). It is returned by Chain.getConn (used by Chain.Conn) when no route is configured, and callers like the SOCKS4 BIND handler explicitly treat it as a benign 'no chain' signal rather than a connection failure.

Source

Thrown at chain.go:16

package gost

import (
	"context"
	"errors"
	"fmt"
	"net"
	"syscall"
	"time"

	"github.com/go-log/log"
)

var (
	// ErrEmptyChain is an error that implies the chain is empty.
	ErrEmptyChain = errors.New("empty chain")
)

// Chain is a proxy chain that holds a list of proxy node groups.
type Chain struct {
	isRoute    bool
	Retries    int
	Mark       int
	Interface  string
	nodeGroups []*NodeGroup
	route      []Node // nodes in the selected route
}

// NewChain creates a proxy chain with a list of proxy nodes.
// It creates the node groups automatically, one group per node.
func NewChain(nodes ...Node) *Chain {
	chain := &Chain{}
	for _, node := range nodes {
		chain.nodeGroups = append(chain.nodeGroups, NewNodeGroup(node))

View on GitHub (pinned to a33fdbf4c9)

Solutions

  1. Add at least one node/group to the Chain (via AppendNode or the -F/--peer config) before dialing through it
  2. Check Chain.IsEmpty() before calling Conn() and skip chaining when empty
  3. Compare returned error against ErrEmptyChain to treat 'no chain' as direct connection instead of failure
  4. Verify config file actually populates the chain field (typo or empty list)

Example fix

// before
cc, err := h.options.Chain.Conn()
if err != nil {
    log.Logf("dial failed: %s", err)
}
// after
cc, err := h.options.Chain.Conn()
if err != nil {
    if errors.Is(err, ErrEmptyChain) {
        cc, err = net.Dial("tcp", target) // direct connection, no chain
    }
    if err != nil {
        log.Logf("dial failed: %s", err)
    }
}
Defensive patterns

Strategy: validation

Validate before calling

if chain == nil || chain.IsEmpty() {
    // no proxy chain configured: dial directly instead of via chain
    return net.Dial("tcp", addr)
}
conn, err := chain.Conn()

Type guard

func chainUsable(c *Chain) bool {
    return c != nil && !c.IsEmpty()
}

Try / catch

conn, err := chain.Conn()
if err != nil {
    if errors.Is(err, ErrEmptyChain) {
        // handle: no chain configured
        return fallbackDial(addr)
    }
    return err
}

Prevention

When it happens

Trigger: Calling Chain.Conn() / getConn on a Chain constructed without any nodes, e.g. &Chain{} or a chain whose node groups were never added via Append/Compose, then using it to dial.

Common situations: Users configure a gost handler without specifying forward/proxy hops, load a config where the chain section is missing or empty, or build a Chain programmatically and forget to add nodes.

Related errors


AI-assisted analysis of ginuerzh/gost@a33fdbf4c9 (2026-09-02). Data as JSON: /api/errors/fbe317ccecda3b16. Report an issue: GitHub.