XTLS/Xray-core · error

not a Service.

Error message

not a Service.

What it means

Thrown by validatePaddingSchedule when a single paddingTurn sets both a list of explicit padding variants (turn.variants) and a send-length range (turn.sendMinLength/turn.sendMaxLength). The padding engine supports two mutually exclusive ways to size the bytes a side sends: precomputed variants or a randomized send range. Combining them is ambiguous, so the schedule is rejected before any traffic flows.

Source

Thrown at app/commander/commander.go:50

		listen: config.Listen,
	}

	common.Must(core.RequireFeatures(ctx, func(om outbound.Manager) {
		c.ohm = om
	}))

	for _, rawConfig := range config.Service {
		config, err := rawConfig.GetInstance()
		if err != nil {
			return nil, err
		}
		rawService, err := common.CreateObject(ctx, config)
		if err != nil {
			return nil, err
		}
		service, ok := rawService.(Service)
		if !ok {
			return nil, errors.New("not a Service.")
		}
		c.services = append(c.services, service)
	}

	return c, nil
}

// Type implements common.HasType.
func (c *Commander) Type() interface{} {
	return (*Commander)(nil)
}

// Start implements common.Runnable.
func (c *Commander) Start() error {
	c.Lock()
	c.server = grpc.NewServer()
	for _, service := range c.services {
		service.Register(c.server)

View on GitHub (pinned to 7d214f8b09)

Solutions

  1. Remove sendMinLength/sendMaxLength from the turn so only variants size the traffic
  2. Or remove the variants array and express the turn purely as minLength/maxLength plus sendMinLength/sendMaxLength
  3. Re-run the connection; validation happens at schedule start, so no restart state is carried over

Example fix

// before
paddingTurn{
  direction: paddingClientToServer,
  variants: []paddingVariant{...},
  sendMinLength: 100,
  sendMaxLength: 400,
}
// after
paddingTurn{
  direction: paddingClientToServer,
  variants: []paddingVariant{...},
}
Defensive patterns

Strategy: validation

Validate before calling

func turnIsConsistent(t paddingTurn) bool {
    hasSendRange := t.sendMinLength != 0 || t.sendMaxLength != 0
    return !(len(t.variants) > 0 && hasSendRange)
}
// before dialing:
for i, t := range schedule {
    if !turnIsConsistent(t) {
        return fmt.Errorf("turn %d mixes variants and send range", i)
    }
}

Prevention

When it happens

Trigger: A paddingTurn literal (or decoded JSON config) that has a non-empty variants array while sendMinLength or sendMaxLength is non-zero. runPaddingSchedule calls validatePaddingSchedule up front, so this fires immediately at handshake/startup, before any network I/O.

Common situations: Copying a variants-based schedule and pasting a send-range block into the same turn; migrating config from range-style padding to variant-style padding without removing the old sendMinLength/sendMaxLength fields; defaults in a shared template that pre-populate sendMinLength.

Related errors


AI-assisted analysis of XTLS/Xray-core@7d214f8b09 (2026-08-15). Data as JSON: /api/errors/78fb9f246427b050. Report an issue: GitHub.