Skip to content

[Bug] VLESS outbound: panic("unknown value") instead of error on invalid packet_encoding #4093

@Leadaxe

Description

@Leadaxe

Operating system

macOS

System version

15.x (darwin/amd64) — bug is platform-independent, reproducible everywhere

Installation type

Original sing-box Command Line

Version

sing-box version unknown

Environment: go1.25.5 darwin/amd64

Built today from master via go install github.com/sagernet/sing-box/cmd/sing-box@latest.
Module versions resolved: sing-box@v1.13.11, sing@v0.8.9. Code paths cited below are
current on dev-next (protocol/vless/outbound.go HEAD as of this writing).

Description

VLESS outbound construction panics with panic: unknown value instead of returning a
normal error when packet_encoding is set to a string that is not one of the three valid
values ("", "xudp", "packetaddr"). The crash takes down the entire process — both
sing-box check and sing-box run exit with a Go panic stack trace, instead of emitting
the intended FATAL initialize outbound[0]: unknown packet encoding: <value>.

This is a clear oversight, not intended behaviour, for two reasons:

1. The intent at the panic site is to return an error.

protocol/vless/outbound.go:79-87:

switch *options.PacketEncoding {
case "":
case "packetaddr":
    outbound.packetAddr = true
case "xudp":
    outbound.xudp = true
default:
    return nil, E.New("unknown packet encoding: ", options.PacketEncoding)
}

The default branch wants to return nil, E.New(...). The panic comes from a missing
deref: options.PacketEncoding is *string (see option/vless.go:26), and the
unreferenced pointer is what gets passed into E.Newformat.ToString. The switch one
line up correctly dereferences (*options.PacketEncoding); only the error-construction
call forgot to.

2. The exact same code in VMess gets it right.

protocol/vmess/outbound.go:74-82:

switch options.PacketEncoding {
case "":
case "packetaddr":
    outbound.packetAddr = true
case "xudp":
    outbound.xudp = true
default:
    return nil, E.New("unknown packet encoding: ", options.PacketEncoding)
}

option/vmess.go:27: PacketEncoding string (not pointer). Same input behaves correctly
— the user gets FATAL initialize outbound[0]: unknown packet encoding: none, no panic.

The only structural difference between the two cases is that VLESS uses *string (to
distinguish "omitted → default xudp" from explicit "" → disabled, see line 76:
if options.PacketEncoding == nil { outbound.xudp = true }), and the deref was forgotten
in the E.New call.

Why the panic, mechanically.

common/format/fmt.go:14-65 (F.ToString) switches on the dynamic type of each argument
and handles string, bool, the integer types, error, and Stringer. There is no
case for pointer types. *string falls through to the default:

default:
    panic("unknown value")

E.New is errors.New(F.ToString(message...)) (common/exceptions/error.go:25-27), so
any E.New call that is handed a pointer crashes the process. This is reachable from
user-controlled config input via the VLESS outbound path described above.

Reproduction

Local, no remote server, no TUN, no GUI. Single config file:

config.json:

{
  "log": {"level": "debug"},
  "outbounds": [
    {
      "type": "vless",
      "tag": "vless-out",
      "server": "127.0.0.1",
      "server_port": 443,
      "uuid": "00000000-0000-0000-0000-000000000000",
      "packet_encoding": "none"
    }
  ]
}

Steps:

  1. go install github.com/sagernet/sing-box/cmd/sing-box@latest
  2. sing-box check -c config.json

Expected: FATAL initialize outbound[0]: unknown packet encoding: none, non-zero exit.

Actual: panic: unknown value, full stack trace below.

For comparison, swapping "type": "vless""type": "vmess" (and adding
"security": "auto") produces the expected FATAL output cleanly.

Logs

Full output of sing-box check -c config.json:

panic: unknown value

goroutine 1 [running]:
github.com/sagernet/sing/common/format.ToString({0xc00073e7e0?, 0x1013f65c0?, 0x1bb?})
	/Users/macbook/go/pkg/mod/github.com/sagernet/sing@v0.8.9/common/format/fmt.go:60 +0x5c5
github.com/sagernet/sing/common/exceptions.New(...)
	/Users/macbook/go/pkg/mod/github.com/sagernet/sing@v0.8.9/common/exceptions/error.go:26
github.com/sagernet/sing-box/protocol/vless.NewOutbound({_, _}, {_, _}, {_, _}, {_, _}, {{{0x0, 0x0}, ...}, ...})
	/Users/macbook/go/pkg/mod/github.com/sagernet/sing-box@v1.13.11/protocol/vless/outbound.go:86 +0x975
github.com/sagernet/sing-box/adapter/outbound.Register[...].func2(...)
	/Users/macbook/go/pkg/mod/github.com/sagernet/sing-box@v1.13.11/adapter/outbound/registry.go:23 +0x15c
github.com/sagernet/sing-box/adapter/outbound.(*Registry).CreateOutbound(...)
	/Users/macbook/go/pkg/mod/github.com/sagernet/sing-box@v1.13.11/adapter/outbound/registry.go:64 +0x255
github.com/sagernet/sing-box/adapter/outbound.(*Manager).Create(...)
	/Users/macbook/go/pkg/mod/github.com/sagernet/sing-box@v1.13.11/adapter/outbound/manager.go:265 +0xcf
github.com/sagernet/sing-box.New(...)
	/Users/macbook/go/pkg/mod/github.com/sagernet/sing-box@v1.13.11/box.go:288 +0x276e
main.check()
	/Users/macbook/go/pkg/mod/github.com/sagernet/sing-box@v1.13.11/cmd/sing-box/cmd_check.go:34 +0x1a5
[...spf13/cobra frames omitted, full trace available on request...]

Why this matters in practice

xray-style subscription generators (xray-knife, manual generators) emit
packetEncoding=none in VLESS share-URIs to mean "no special encoding" — semantically
the same as omitted, but a literal string in URI form. Subscription clients that
faithfully translate URI → outbound JSON pass "packet_encoding": "none" straight into
sing-box, which panics during outbound construction. Affected clients have to add
client-side allow-list filtering to avoid feeding this crash. Fixing it upstream removes
the foot-gun for every downstream client.

Suggested direction (no PR attached)

Two independent one-liners, either of which fixes the immediate report; both are worth
having for defence in depth:

  1. Deref at the call site. protocol/vless/outbound.go:86:
    E.New("unknown packet encoding: ", *options.PacketEncoding) — matches the deref
    already done by the switch above and the error message style used by VMess.

  2. Make format.ToString graceful for pointer types. Add a reflect-based pointer
    handler (or explicit case *string/case *int/...) in common/format/fmt.go. This
    protects every other E.New call site in the codebase from the same class of bug,
    not just this one.

Happy to clarify any of the above.

Supporter

Integrity requirements

  • I confirm that I have read the documentation, understand the meaning of all the configuration items I wrote, and did not pile up seemingly useful options or default values.
  • I confirm that I have provided the server and client configuration files and process that can be reproduced locally, instead of a complicated client configuration file that has been stripped of sensitive data.
  • I confirm that I have provided the simplest configuration that can be used to reproduce the error I reported, instead of depending on remote servers, TUN, graphical interface clients, or other closed-source software.
  • I confirm that I have provided the complete configuration files and logs, rather than just providing parts I think are useful out of confidence in my own intelligence.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions