Files
kazeyukiro a46b3b010b feat(listener): HTTP/SOCKS inbounds with client share and Clash proxies
Re-apply on post-theme baseline (52e848a): schema, compile modules,
http:// and socks5:// share URIs, and client subscription YAML
(type: http / socks5). Fixes gofmt for CI.
2026-10-07 16:42:27 +00:00

398 lines
13 KiB
Go

package protocol
import (
"crypto/rand"
"encoding/hex"
"fmt"
"strings"
)
// GenericCompiler passes config through with optional users injection.
type GenericCompiler struct{ kind string }
func (g GenericCompiler) Kind() string { return g.kind }
func (g GenericCompiler) Capability() ProtocolCapability {
return ProtocolCapability{Kind: g.kind, Label: g.kind}
}
func (g GenericCompiler) Compile(in CompileInput) (map[string]interface{}, error) {
m := baseMap(in)
copyConfigPassthrough(m, in.Config, managedKeys())
if in.UDP {
m["udp"] = true
}
if in.TLS {
m["tls"] = true
}
return m, nil
}
type VLESSCompiler struct{}
func (VLESSCompiler) Kind() string { return "vless" }
func (VLESSCompiler) Capability() ProtocolCapability { return vlessCapability() }
func (VLESSCompiler) Compile(in CompileInput) (map[string]interface{}, error) {
m := baseMap(in)
skip := managedKeys()
copyConfigPassthrough(m, in.Config, skip)
// Per official MetaCubeX wiki (inbound-vless), the listener YAML does NOT
// carry a top-level `udp` field — mihomo allows UDP by default when absent.
delete(m, "udp")
// Official VLESS listener has no top-level "tls" field; TLS is implied by
// certificate/private-key, reality-config, or wrappers — never emit tls: true.
delete(m, "tls")
users := asUsersArray(in.Config, in.Users, "uuid", in.HasCredentialState)
if len(users) > 0 {
m["users"] = users
}
return m, nil
}
type VMessCompiler struct{}
func (VMessCompiler) Kind() string { return "vmess" }
func (VMessCompiler) Capability() ProtocolCapability { return vmessCapability() }
func (VMessCompiler) Compile(in CompileInput) (map[string]interface{}, error) {
m := baseMap(in)
copyConfigPassthrough(m, in.Config, managedKeys())
// Per official MetaCubeX wiki (inbound-vmess), the listener YAML does NOT
// carry a top-level `udp` field — mihomo allows UDP by default when absent.
delete(m, "udp")
// Official VMess listener: no top-level tls boolean.
delete(m, "tls")
users := asUsersArray(in.Config, in.Users, "uuid", in.HasCredentialState)
if len(users) > 0 {
m["users"] = users
}
return m, nil
}
type TrojanCompiler struct{}
func (TrojanCompiler) Kind() string { return "trojan" }
func (TrojanCompiler) Capability() ProtocolCapability { return trojanCapability() }
func (TrojanCompiler) Compile(in CompileInput) (map[string]interface{}, error) {
m := baseMap(in)
copyConfigPassthrough(m, in.Config, managedKeys())
// Per official MetaCubeX wiki (inbound-trojan), the listener YAML does NOT
// carry a top-level `udp` field — mihomo allows UDP by default when absent.
delete(m, "udp")
// Official Trojan listener: no top-level tls boolean.
delete(m, "tls")
users := asUsersArray(in.Config, in.Users, "password", in.HasCredentialState)
if len(users) > 0 {
m["users"] = users
}
return m, nil
}
type ShadowsocksCompiler struct{}
func (ShadowsocksCompiler) Kind() string { return "shadowsocks" }
func (ShadowsocksCompiler) Capability() ProtocolCapability { return shadowsocksCapability() }
func (ShadowsocksCompiler) Compile(in CompileInput) (map[string]interface{}, error) {
m := baseMap(in)
copyConfigPassthrough(m, in.Config, managedKeys())
// Mihomo defaults the common listener udp field to true. Emit both values
// so disabling UDP in the panel is preserved instead of silently falling
// back to the core default after each config regeneration.
m["udp"] = in.UDP
if len(in.Users) > 1 {
return nil, fmt.Errorf("shadowsocks supports one password; %d credentials bound", len(in.Users))
}
if len(in.Users) == 1 && in.Users[0].Password != "" {
m["password"] = in.Users[0].Password
}
return m, nil
}
type Hysteria2Compiler struct{}
func (Hysteria2Compiler) Kind() string { return "hysteria2" }
func (Hysteria2Compiler) Capability() ProtocolCapability { return hysteria2Capability() }
func (Hysteria2Compiler) Compile(in CompileInput) (map[string]interface{}, error) {
m := baseMap(in)
copyConfigPassthrough(m, in.Config, managedKeys())
users := asUsersMap(in.Config, in.Users, in.HasCredentialState)
if len(users) > 0 {
m["users"] = users
}
return m, nil
}
type TUICCompiler struct{ kind string }
func (t TUICCompiler) Kind() string {
if t.kind != "" {
return t.kind
}
return "tuic"
}
func (TUICCompiler) Capability() ProtocolCapability { return tuicCapability() }
func (t TUICCompiler) Compile(in CompileInput) (map[string]interface{}, error) {
m := baseMap(in)
// Mihomo's listener type is always "tuic" regardless of v4/v5.
m["type"] = "tuic"
copyConfigPassthrough(m, in.Config, managedKeys())
// Mihomo expects token as a slice. Legacy configs may store a single string.
if tok, ok := m["token"].(string); ok {
m["token"] = []string{tok}
}
// Official Mihomo: always type "tuic". Auth mode is token (v4) vs users (v5).
// Panel protocol names: tuic-v4 / tuic-v5. Legacy "tuic" auto-detects.
kind := t.Kind()
toks := allStringTokens(m["token"])
hasUsers := false
if um, ok := m["users"].(map[string]interface{}); ok && len(um) > 0 {
hasUsers = true
}
isV4 := kind == "tuic-v4"
if kind == "tuic-v5" {
isV4 = false
} else if kind == "tuic" {
// Wiki: exactly one of token or users. Prefer explicit token-only as v4.
isV4 = len(toks) > 0 && !hasUsers
}
if isV4 {
if len(toks) == 0 {
toks = []string{randomToken()}
}
m["token"] = toks
delete(m, "users")
} else {
delete(m, "token")
users := asUsersMapUUID(in.Config, in.Users, in.HasCredentialState)
if len(users) > 0 {
m["users"] = users
}
}
// Sensible defaults when operator left advanced QUIC fields empty.
if _, ok := m["alpn"]; !ok {
m["alpn"] = []string{"h3"}
}
if _, ok := m["congestion-controller"]; !ok {
m["congestion-controller"] = "bbr"
}
return m, nil
}
type ShadowQUICCompiler struct{}
func (ShadowQUICCompiler) Kind() string { return "shadowquic" }
func (ShadowQUICCompiler) Capability() ProtocolCapability { return shadowquicCapability() }
func (ShadowQUICCompiler) Compile(in CompileInput) (map[string]interface{}, error) {
m := baseMap(in)
copyConfigPassthrough(m, in.Config, managedKeys())
users := asUsersArray(in.Config, in.Users, "password", in.HasCredentialState)
if len(users) > 0 {
m["users"] = users
}
if _, ok := m["congestion-controller"]; !ok {
m["congestion-controller"] = "cubic"
}
return m, nil
}
type TrustTunnelCompiler struct{}
func (TrustTunnelCompiler) Kind() string { return "trusttunnel" }
func (TrustTunnelCompiler) Capability() ProtocolCapability {
return ProtocolCapability{Kind: "trusttunnel", Label: "trusttunnel"}
}
func (TrustTunnelCompiler) Compile(in CompileInput) (map[string]interface{}, error) {
m := baseMap(in)
copyConfigPassthrough(m, in.Config, managedKeys())
// TrustTunnel uses users: [{username, password}] (array form, verified against Mihomo wiki).
users := asUsersArray(in.Config, in.Users, "password", in.HasCredentialState)
if len(users) > 0 {
m["users"] = users
}
// Mihomo expects network as a slice (e.g. ["tcp", "udp"]).
// Legacy configs may store it as a string; convert to slice.
if net, ok := m["network"].(string); ok {
m["network"] = []string{net}
}
return m, nil
}
type AnyTLSCompiler struct{}
func (AnyTLSCompiler) Kind() string { return "anytls" }
func (AnyTLSCompiler) Capability() ProtocolCapability {
return ProtocolCapability{Kind: "anytls", Label: "anytls"}
}
func (AnyTLSCompiler) Compile(in CompileInput) (map[string]interface{}, error) {
m := baseMap(in)
copyConfigPassthrough(m, in.Config, managedKeys())
if in.UDP {
m["udp"] = true
}
users := asUsersMap(in.Config, in.Users, in.HasCredentialState)
if len(users) > 0 {
m["users"] = users
}
return m, nil
}
type MieruCompiler struct{}
func (MieruCompiler) Kind() string { return "mieru" }
func (MieruCompiler) Capability() ProtocolCapability {
return ProtocolCapability{Kind: "mieru", Label: "mieru"}
}
func (MieruCompiler) Compile(in CompileInput) (map[string]interface{}, error) {
m := baseMap(in)
copyConfigPassthrough(m, in.Config, managedKeys())
users := asUsersMap(in.Config, in.Users, in.HasCredentialState)
if len(users) > 0 {
m["users"] = users
}
return m, nil
}
func randomToken() string {
b := make([]byte, 16)
_, _ = rand.Read(b)
return hex.EncodeToString(b)
}
func firstStringToken(tok interface{}) string {
switch v := tok.(type) {
case string:
return strings.TrimSpace(v)
case []string:
for _, s := range v {
if s = strings.TrimSpace(s); s != "" {
return s
}
}
case []interface{}:
for _, item := range v {
if s, ok := item.(string); ok {
if s = strings.TrimSpace(s); s != "" {
return s
}
}
}
}
return ""
}
// allStringTokens returns every non-empty trimmed string token from a
// listener Config `token` field. Mihomo's TUIC v4 spec accepts multiple
// tokens (`token: [T1, T2, …]`); any one authenticates the client.
// Used by TUIC v4 compiler to preserve the full array instead of dropping
// all but the first.
func allStringTokens(tok interface{}) []string {
switch v := tok.(type) {
case string:
if s := strings.TrimSpace(v); s != "" {
return []string{s}
}
case []string:
out := make([]string, 0, len(v))
for _, s := range v {
if s = strings.TrimSpace(s); s != "" {
out = append(out, s)
}
}
return out
case []interface{}:
out := make([]string, 0, len(v))
for _, item := range v {
if s, ok := item.(string); ok {
if s = strings.TrimSpace(s); s != "" {
out = append(out, s)
}
}
}
return out
}
return nil
}
// HTTPCompiler emits a Mihomo HTTP inbound listener.
// Wiki: https://wiki.metacubex.one/config/inbound/listeners/http/
// users: [{username, password}]; empty users: [] skips auth for this inbound.
type HTTPCompiler struct{}
func (HTTPCompiler) Kind() string { return "http" }
func (HTTPCompiler) Capability() ProtocolCapability {
return ProtocolCapability{
Kind: "http",
Label: "HTTP",
Fields: []FieldCapability{
{Path: "certificate", Label: "Certificate", Type: FieldText, Advanced: true, Description: "TLS cert PEM or path (with private-key enables TLS)."},
{Path: "private-key", Label: "Private Key", Type: FieldSecret, Advanced: true, Description: "TLS private key PEM or path."},
{Path: "client-auth-type", Label: "Client Auth Type", Type: FieldString, Advanced: true, Options: []string{"", "request", "require-any", "verify-if-given", "require-and-verify"}},
{Path: "client-auth-cert", Label: "Client Auth Cert", Type: FieldText, Advanced: true},
{Path: "ech-key", Label: "ECH Key", Type: FieldText, Advanced: true},
},
UserFields: []FieldCapability{
{Path: "username", Label: "Username", Type: FieldString, Required: true},
{Path: "password", Label: "Password", Type: FieldSecret, Required: true},
},
Features: []string{"tcp"},
}
}
func (HTTPCompiler) Compile(in CompileInput) (map[string]interface{}, error) {
m := baseMap(in)
skip := managedKeys()
copyConfigPassthrough(m, in.Config, skip)
delete(m, "udp") // HTTP listener has no udp field
delete(m, "tls")
users := asUsersArray(in.Config, in.Users, "password", in.HasCredentialState)
if in.HasCredentialState || len(in.Users) > 0 {
if users == nil {
users = []map[string]interface{}{}
}
m["users"] = users
} else if raw, ok := in.Config["users"]; ok {
m["users"] = raw
}
return m, nil
}
// SocksCompiler emits a Mihomo SOCKS inbound listener.
// Wiki: https://wiki.metacubex.one/config/inbound/listeners/socks/
type SocksCompiler struct{}
func (SocksCompiler) Kind() string { return "socks" }
func (SocksCompiler) Capability() ProtocolCapability {
return ProtocolCapability{
Kind: "socks",
Label: "SOCKS",
Fields: []FieldCapability{
{Path: "udp", Label: "UDP", Type: FieldBoolean, Description: "Listen for UDP (wiki: udp)."},
{Path: "certificate", Label: "Certificate", Type: FieldText, Advanced: true},
{Path: "private-key", Label: "Private Key", Type: FieldSecret, Advanced: true},
{Path: "client-auth-type", Label: "Client Auth Type", Type: FieldString, Advanced: true, Options: []string{"", "request", "require-any", "verify-if-given", "require-and-verify"}},
{Path: "client-auth-cert", Label: "Client Auth Cert", Type: FieldText, Advanced: true},
{Path: "ech-key", Label: "ECH Key", Type: FieldText, Advanced: true},
},
UserFields: []FieldCapability{
{Path: "username", Label: "Username", Type: FieldString, Required: true},
{Path: "password", Label: "Password", Type: FieldSecret, Required: true},
},
Features: []string{"tcp", "udp"},
}
}
func (SocksCompiler) Compile(in CompileInput) (map[string]interface{}, error) {
m := baseMap(in)
skip := managedKeys()
copyConfigPassthrough(m, in.Config, skip)
delete(m, "tls")
if in.UDP {
m["udp"] = true
}
users := asUsersArray(in.Config, in.Users, "password", in.HasCredentialState)
if in.HasCredentialState || len(in.Users) > 0 {
if users == nil {
users = []map[string]interface{}{}
}
m["users"] = users
} else if raw, ok := in.Config["users"]; ok {
m["users"] = raw
}
return m, nil
}