9ccf402ed1
The stdio listener wraps os.Stdin/os.Stdout as a net.Listener that accepts exactly one connection. It enables GOST to be used as an SSH ProxyCommand, where the parent process (SSH) pipes the transport byte stream through the child process's standard I/O. Usage: gost -L stdio://example.com:22 -F http://proxy:8080 The URL host:port is treated as the forwarding target and converted to a forward node in buildServiceConfig. The standard forward handler then routes traffic through the configured chain to the destination. Fixes go-gost/gost#433
957 lines
23 KiB
Go
957 lines
23 KiB
Go
// Package cmd converts CLI command-line arguments (-L services, -F chain nodes)
|
|
// into structured config.Config objects. It supports URL-format service and node
|
|
// specifications, metadata query parameters, and auto-detection of handler,
|
|
// listener, dialer, and connector types via the registry.
|
|
package cmd
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"net/url"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-gost/x/config"
|
|
xnet "github.com/go-gost/x/internal/net"
|
|
"github.com/go-gost/x/limiter/conn"
|
|
"github.com/go-gost/x/limiter/traffic"
|
|
mdx "github.com/go-gost/x/metadata"
|
|
mdutil "github.com/go-gost/x/metadata/util"
|
|
"github.com/go-gost/x/registry"
|
|
)
|
|
|
|
var (
|
|
// ErrInvalidCmd is returned when the command-line argument is empty.
|
|
ErrInvalidCmd = errors.New("invalid cmd")
|
|
// ErrInvalidNode is returned when a node specification cannot be parsed.
|
|
ErrInvalidNode = errors.New("invalid node")
|
|
)
|
|
|
|
// BuildConfigFromCmd converts CLI service (-L) and node (-F) arguments into
|
|
// a Config. Each node becomes a hop in a single chain; each service produces
|
|
// a ServiceConfig with auto-detected handler and listener types. Query
|
|
// parameters in the URL are parsed as metadata and routed to the appropriate
|
|
// config sub-struct (handler, listener, hop, connector, dialer, service).
|
|
func BuildConfigFromCmd(serviceList, nodeList []string) (*config.Config, error) {
|
|
namePrefix := ""
|
|
cfg := &config.Config{}
|
|
|
|
var chain *config.ChainConfig
|
|
if len(nodeList) > 0 {
|
|
chain = &config.ChainConfig{
|
|
Name: fmt.Sprintf("%schain-0", namePrefix),
|
|
}
|
|
cfg.Chains = append(cfg.Chains, chain)
|
|
}
|
|
|
|
for i, node := range nodeList {
|
|
url, err := Norm(node)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
m := map[string]any{}
|
|
for k, v := range url.Query() {
|
|
if len(v) > 0 {
|
|
m[k] = v[0]
|
|
}
|
|
}
|
|
md := mdx.NewMetadata(m)
|
|
|
|
hopConfig := &config.HopConfig{
|
|
Name: fmt.Sprintf("%shop-%d", namePrefix, i),
|
|
Selector: parseSelector(m),
|
|
}
|
|
|
|
if v := mdutil.GetString(md, "bypass"); v != "" {
|
|
bypassCfg := &config.BypassConfig{
|
|
Name: fmt.Sprintf("%sbypass-%d", namePrefix, len(cfg.Bypasses)),
|
|
}
|
|
if v[0] == '~' {
|
|
bypassCfg.Whitelist = true
|
|
v = v[1:]
|
|
}
|
|
for _, s := range strings.Split(v, ",") {
|
|
if s == "" {
|
|
continue
|
|
}
|
|
bypassCfg.Matchers = append(bypassCfg.Matchers, s)
|
|
}
|
|
hopConfig.Bypass = bypassCfg.Name
|
|
cfg.Bypasses = append(cfg.Bypasses, bypassCfg)
|
|
delete(m, "bypass")
|
|
}
|
|
if v := mdutil.GetString(md, "resolver"); v != "" {
|
|
resolverCfg := &config.ResolverConfig{
|
|
Name: fmt.Sprintf("%sresolver-%d", namePrefix, len(cfg.Resolvers)),
|
|
}
|
|
for _, rs := range strings.Split(v, ",") {
|
|
if rs == "" {
|
|
continue
|
|
}
|
|
resolverCfg.Nameservers = append(
|
|
resolverCfg.Nameservers,
|
|
&config.NameserverConfig{
|
|
Addr: rs,
|
|
},
|
|
)
|
|
}
|
|
hopConfig.Resolver = resolverCfg.Name
|
|
cfg.Resolvers = append(cfg.Resolvers, resolverCfg)
|
|
delete(m, "resolver")
|
|
}
|
|
if v := mdutil.GetString(md, "hosts"); v != "" {
|
|
hostsCfg := &config.HostsConfig{
|
|
Name: fmt.Sprintf("%shosts-%d", namePrefix, len(cfg.Hosts)),
|
|
}
|
|
for _, s := range strings.Split(v, ",") {
|
|
ss := strings.SplitN(s, ":", 2)
|
|
if len(ss) != 2 {
|
|
continue
|
|
}
|
|
hostsCfg.Mappings = append(
|
|
hostsCfg.Mappings,
|
|
&config.HostMappingConfig{
|
|
Hostname: ss[0],
|
|
IP: ss[1],
|
|
},
|
|
)
|
|
}
|
|
hopConfig.Hosts = hostsCfg.Name
|
|
cfg.Hosts = append(cfg.Hosts, hostsCfg)
|
|
delete(m, "hosts")
|
|
}
|
|
|
|
if v := mdutil.GetString(md, "interface"); v != "" {
|
|
m["hop.interface"] = v
|
|
delete(m, "interface")
|
|
}
|
|
if v := mdutil.GetInt(md, "so_mark"); v > 0 {
|
|
m["hop.so_mark"] = v
|
|
delete(m, "so_mark")
|
|
}
|
|
if v := mdutil.GetInt(md, "proxyProtocol"); v > 0 {
|
|
m["hop.proxyProtocol"] = v
|
|
delete(m, "proxyProtocol")
|
|
}
|
|
|
|
hopMd := map[string]any{}
|
|
for k, v := range m {
|
|
if strings.HasPrefix(k, "node.") ||
|
|
strings.HasPrefix(k, "connector.") ||
|
|
strings.HasPrefix(k, "dialer.") {
|
|
continue
|
|
}
|
|
if s, found := strings.CutPrefix(k, "hop."); found {
|
|
hopMd[s] = v
|
|
delete(m, k)
|
|
continue
|
|
}
|
|
|
|
hopMd[k] = v
|
|
}
|
|
hopConfig.Metadata = hopMd
|
|
|
|
nodeConfig, err := buildNodeConfig(url, m)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nodeConfig.Name = fmt.Sprintf("%snode-0", namePrefix)
|
|
|
|
var nodes []*config.NodeConfig
|
|
for _, host := range strings.Split(nodeConfig.Addr, ",") {
|
|
if host == "" {
|
|
continue
|
|
}
|
|
nodeCfg := &config.NodeConfig{}
|
|
*nodeCfg = *nodeConfig
|
|
nodeCfg.Connector = copyConnectorConfig(nodeConfig.Connector)
|
|
nodeCfg.Dialer = copyDialerConfig(nodeConfig.Dialer)
|
|
nodeCfg.Metadata = copyMap(nodeConfig.Metadata)
|
|
nodeCfg.Name = fmt.Sprintf("%snode-%d", namePrefix, len(nodes))
|
|
nodeCfg.Addr = host
|
|
nodes = append(nodes, nodeCfg)
|
|
}
|
|
hopConfig.Nodes = nodes
|
|
|
|
chain.Hops = append(chain.Hops, hopConfig)
|
|
}
|
|
|
|
var services []*config.ServiceConfig
|
|
for _, svc := range serviceList {
|
|
svc = strings.TrimSpace(svc)
|
|
if svc == "" {
|
|
continue
|
|
}
|
|
|
|
if svc[0] == ':' || !strings.Contains(svc, "://") {
|
|
svc = "auto://" + svc
|
|
}
|
|
|
|
host, svc := cutHost(svc)
|
|
|
|
url, err := Norm(svc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
url.Host = host
|
|
|
|
svcs, err := buildServiceConfig(url)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
services = append(services, svcs...)
|
|
}
|
|
|
|
for i, service := range services {
|
|
service.Name = fmt.Sprintf("%sservice-%d", namePrefix, i)
|
|
if chain != nil {
|
|
if service.Listener.Type == "rtcp" || service.Listener.Type == "rudp" || service.Listener.Type == "runix" {
|
|
service.Listener.Chain = chain.Name
|
|
} else {
|
|
service.Handler.Chain = chain.Name
|
|
}
|
|
}
|
|
cfg.Services = append(cfg.Services, service)
|
|
|
|
mh := service.Handler.Metadata
|
|
md := mdx.NewMetadata(mh)
|
|
if v := mdutil.GetInt(md, "retries"); v > 0 {
|
|
service.Handler.Retries = v
|
|
delete(mh, "retries")
|
|
}
|
|
if v := mdutil.GetString(md, "admission"); v != "" {
|
|
admCfg := &config.AdmissionConfig{
|
|
Name: fmt.Sprintf("%sadmission-%d", namePrefix, len(cfg.Admissions)),
|
|
}
|
|
if v[0] == '~' {
|
|
admCfg.Whitelist = true
|
|
v = v[1:]
|
|
}
|
|
for _, s := range strings.Split(v, ",") {
|
|
if s == "" {
|
|
continue
|
|
}
|
|
admCfg.Matchers = append(admCfg.Matchers, s)
|
|
}
|
|
service.Admission = admCfg.Name
|
|
cfg.Admissions = append(cfg.Admissions, admCfg)
|
|
delete(mh, "admission")
|
|
}
|
|
if v := mdutil.GetString(md, "bypass"); v != "" {
|
|
bypassCfg := &config.BypassConfig{
|
|
Name: fmt.Sprintf("%sbypass-%d", namePrefix, len(cfg.Bypasses)),
|
|
}
|
|
if v[0] == '~' {
|
|
bypassCfg.Whitelist = true
|
|
v = v[1:]
|
|
}
|
|
for _, s := range strings.Split(v, ",") {
|
|
if s == "" {
|
|
continue
|
|
}
|
|
bypassCfg.Matchers = append(bypassCfg.Matchers, s)
|
|
}
|
|
service.Bypass = bypassCfg.Name
|
|
cfg.Bypasses = append(cfg.Bypasses, bypassCfg)
|
|
delete(mh, "bypass")
|
|
}
|
|
if v := mdutil.GetString(md, "resolver"); v != "" {
|
|
resolverCfg := &config.ResolverConfig{
|
|
Name: fmt.Sprintf("%sresolver-%d", namePrefix, len(cfg.Resolvers)),
|
|
}
|
|
for _, rs := range strings.Split(v, ",") {
|
|
if rs == "" {
|
|
continue
|
|
}
|
|
resolverCfg.Nameservers = append(
|
|
resolverCfg.Nameservers,
|
|
&config.NameserverConfig{
|
|
Addr: rs,
|
|
Prefer: mdutil.GetString(md, "prefer"),
|
|
},
|
|
)
|
|
}
|
|
service.Resolver = resolverCfg.Name
|
|
cfg.Resolvers = append(cfg.Resolvers, resolverCfg)
|
|
delete(mh, "resolver")
|
|
}
|
|
if v := mdutil.GetString(md, "hosts"); v != "" {
|
|
hostsCfg := &config.HostsConfig{
|
|
Name: fmt.Sprintf("%shosts-%d", namePrefix, len(cfg.Hosts)),
|
|
}
|
|
for _, s := range strings.Split(v, ",") {
|
|
ss := strings.SplitN(s, ":", 2)
|
|
if len(ss) != 2 {
|
|
continue
|
|
}
|
|
hostsCfg.Mappings = append(
|
|
hostsCfg.Mappings,
|
|
&config.HostMappingConfig{
|
|
Hostname: ss[0],
|
|
IP: ss[1],
|
|
},
|
|
)
|
|
}
|
|
service.Hosts = hostsCfg.Name
|
|
cfg.Hosts = append(cfg.Hosts, hostsCfg)
|
|
delete(mh, "hosts")
|
|
}
|
|
|
|
in := mdutil.GetString(md, "limiter.in")
|
|
out := mdutil.GetString(md, "limiter.out")
|
|
cin := mdutil.GetString(md, "limiter.conn.in")
|
|
cout := mdutil.GetString(md, "limiter.conn.out")
|
|
if in != "" || cin != "" || out != "" || cout != "" {
|
|
limiter := &config.LimiterConfig{
|
|
Name: fmt.Sprintf("%slimiter-%d", namePrefix, len(cfg.Limiters)),
|
|
}
|
|
if in != "" || out != "" {
|
|
limiter.Limits = append(limiter.Limits,
|
|
fmt.Sprintf("%s %s %s", traffic.ServiceLimitKey, in, out))
|
|
}
|
|
if cin != "" || cout != "" {
|
|
limiter.Limits = append(limiter.Limits,
|
|
fmt.Sprintf("%s %s %s", traffic.ConnLimitKey, cin, cout))
|
|
}
|
|
service.Limiter = limiter.Name
|
|
cfg.Limiters = append(cfg.Limiters, limiter)
|
|
delete(mh, "limiter.in")
|
|
delete(mh, "limiter.out")
|
|
delete(mh, "limiter.conn.in")
|
|
delete(mh, "limiter.conn.out")
|
|
}
|
|
|
|
if climit := mdutil.GetInt(md, "climiter"); climit > 0 {
|
|
limiter := &config.LimiterConfig{
|
|
Name: fmt.Sprintf("%sclimiter-%d", namePrefix, len(cfg.CLimiters)),
|
|
Limits: []string{fmt.Sprintf("%s %d", conn.GlobalLimitKey, climit)},
|
|
}
|
|
service.CLimiter = limiter.Name
|
|
cfg.CLimiters = append(cfg.CLimiters, limiter)
|
|
delete(mh, "climiter")
|
|
}
|
|
|
|
if rlimit := mdutil.GetFloat(md, "rlimiter"); rlimit > 0 {
|
|
limiter := &config.LimiterConfig{
|
|
Name: fmt.Sprintf("%srlimiter-%d", namePrefix, len(cfg.RLimiters)),
|
|
Limits: []string{fmt.Sprintf("%s %s", conn.GlobalLimitKey, strconv.FormatFloat(rlimit, 'f', -1, 64))},
|
|
}
|
|
service.RLimiter = limiter.Name
|
|
cfg.RLimiters = append(cfg.RLimiters, limiter)
|
|
delete(mh, "rlimiter")
|
|
}
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
// cutHost extracts the host portion from a URL string, stripping any auth
|
|
// info (user:pass@). It returns the host and the remainder of the URL with
|
|
// the host portion removed.
|
|
func cutHost(s string) (host, remain string) {
|
|
if s == "" {
|
|
return
|
|
}
|
|
|
|
n := strings.IndexByte(s, ':')
|
|
start := n + 3
|
|
end := strings.IndexAny(s[start:], "/?")
|
|
if end < 0 {
|
|
end = len(s)
|
|
} else {
|
|
end += start
|
|
}
|
|
// auth info
|
|
if n = strings.LastIndexByte(s[start:end], '@'); n >= 0 {
|
|
start += (n + 1)
|
|
}
|
|
host = s[start:end]
|
|
|
|
remain = s[:start] + s[end:]
|
|
|
|
return
|
|
}
|
|
|
|
// buildServiceConfig converts a parsed service URL into one or more
|
|
// ServiceConfig entries. Scheme components (handler+listener) are resolved
|
|
// against the registry; path-based protocols use the URL path as the listen
|
|
// address; a non-empty path for other protocols indicates forwarding mode.
|
|
func buildServiceConfig(url *url.URL) ([]*config.ServiceConfig, error) {
|
|
namePrefix := ""
|
|
if v := os.Getenv("_GOST_ID"); v != "" {
|
|
namePrefix = fmt.Sprintf("go-%s@", v)
|
|
}
|
|
|
|
var handler, listener string
|
|
schemes := strings.Split(url.Scheme, "+")
|
|
if len(schemes) == 1 {
|
|
handler = schemes[0]
|
|
listener = schemes[0]
|
|
}
|
|
if len(schemes) == 2 {
|
|
handler = schemes[0]
|
|
listener = schemes[1]
|
|
}
|
|
|
|
// For path-based protocols (Unix socket listeners and serial), use path as address
|
|
isPathBasedProtocol := listener == "unix" || listener == "runix" || listener == "serial"
|
|
|
|
var addrs []string
|
|
if isPathBasedProtocol {
|
|
path := url.EscapedPath()
|
|
if url.Host != "" {
|
|
addrs = append(addrs, url.Host+strings.TrimPrefix(path, "/"))
|
|
} else if path != "" {
|
|
addrs = append(addrs, path)
|
|
} else {
|
|
addrs = append(addrs, url.Host)
|
|
}
|
|
} else {
|
|
addrs = xnet.AddrPortRange(url.Host).Addrs()
|
|
if len(addrs) == 0 {
|
|
addrs = append(addrs, url.Host)
|
|
}
|
|
}
|
|
|
|
var services []*config.ServiceConfig
|
|
for _, addr := range addrs {
|
|
services = append(services, &config.ServiceConfig{
|
|
Addr: addr,
|
|
})
|
|
}
|
|
|
|
if h := registry.HandlerRegistry().Get(handler); h == nil {
|
|
handler = "auto"
|
|
}
|
|
if ln := registry.ListenerRegistry().Get(listener); ln == nil {
|
|
listener = "tcp"
|
|
if handler == "ssu" {
|
|
listener = "udp"
|
|
}
|
|
}
|
|
|
|
var nodes []*config.ForwardNodeConfig
|
|
// forward mode
|
|
// For path-based protocols, path is address not forward target
|
|
if remotes := strings.Trim(url.EscapedPath(), "/"); remotes != "" && !isPathBasedProtocol {
|
|
i := 0
|
|
for _, addr := range strings.Split(remotes, ",") {
|
|
addrs := xnet.AddrPortRange(addr).Addrs()
|
|
if len(addrs) == 0 {
|
|
addrs = append(addrs, addr)
|
|
}
|
|
for _, adr := range addrs {
|
|
nodes = append(nodes, &config.ForwardNodeConfig{
|
|
Name: fmt.Sprintf("%starget-%d", namePrefix, i),
|
|
Addr: adr,
|
|
})
|
|
i++
|
|
}
|
|
}
|
|
|
|
if handler != "relay" {
|
|
if listener == "tcp" || listener == "udp" ||
|
|
listener == "rtcp" || listener == "rudp" ||
|
|
listener == "tun" || listener == "tap" ||
|
|
listener == "dns" || listener == "unix" || listener == "runix" ||
|
|
listener == "serial" {
|
|
handler = listener
|
|
} else {
|
|
handler = "forward"
|
|
}
|
|
}
|
|
}
|
|
|
|
// stdio listener uses url.Host as the forwarding target.
|
|
if listener == "stdio" && len(nodes) == 0 && url.Host != "" {
|
|
nodes = append(nodes, &config.ForwardNodeConfig{
|
|
Name: fmt.Sprintf("%starget-0", namePrefix),
|
|
Addr: url.Host,
|
|
})
|
|
if handler == "auto" {
|
|
handler = "tcp"
|
|
}
|
|
}
|
|
|
|
if len(nodes) > 0 {
|
|
if len(services) == 1 {
|
|
services[0].Forwarder = &config.ForwarderConfig{
|
|
Nodes: nodes,
|
|
}
|
|
} else {
|
|
for i, svc := range services {
|
|
if len(nodes) == 1 {
|
|
svc.Forwarder = &config.ForwarderConfig{
|
|
Nodes: nodes,
|
|
}
|
|
} else {
|
|
if i < len(nodes) {
|
|
svc.Forwarder = &config.ForwarderConfig{
|
|
Nodes: []*config.ForwardNodeConfig{nodes[i]},
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var auth *config.AuthConfig
|
|
if url.User != nil {
|
|
if strings.HasPrefix(url.Scheme, "ss") {
|
|
auth = decodeSIP002Auth(url)
|
|
}
|
|
if auth == nil {
|
|
auth = &config.AuthConfig{
|
|
Username: url.User.Username(),
|
|
}
|
|
auth.Password, _ = url.User.Password()
|
|
}
|
|
}
|
|
|
|
m := map[string]any{}
|
|
for k, v := range url.Query() {
|
|
if len(v) > 0 {
|
|
m[k] = v[0]
|
|
}
|
|
}
|
|
md := mdx.NewMetadata(m)
|
|
|
|
if sa := mdutil.GetString(md, "auth"); sa != "" && auth == nil {
|
|
au, err := parseAuthFromCmd(sa)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
auth = au
|
|
}
|
|
delete(m, "auth")
|
|
|
|
tlsConfig := &config.TLSConfig{
|
|
CertFile: mdutil.GetString(md, "tls.certFile", "certFile", "cert"),
|
|
KeyFile: mdutil.GetString(md, "tls.keyFile", "keyFile", "key"),
|
|
CAFile: mdutil.GetString(md, "tls.caFile", "caFile", "ca"),
|
|
}
|
|
|
|
delete(m, "tls.certFile")
|
|
delete(m, "certFile")
|
|
delete(m, "cert")
|
|
delete(m, "tls.keyFile")
|
|
delete(m, "keyFile")
|
|
delete(m, "key")
|
|
delete(m, "tls.caFile")
|
|
delete(m, "caFile")
|
|
delete(m, "ca")
|
|
|
|
if tlsConfig.CertFile == "" {
|
|
tlsConfig = nil
|
|
}
|
|
|
|
if v := mdutil.GetString(md, "dns"); v != "" {
|
|
m["dns"] = strings.Split(v, ",")
|
|
}
|
|
|
|
if v := mdutil.GetString(md, "interface"); v != "" {
|
|
m["service.interface"] = v
|
|
delete(m, "interface")
|
|
}
|
|
if v := mdutil.GetInt(md, "so_mark"); v > 0 {
|
|
m["service.so_mark"] = v
|
|
delete(m, "so_mark")
|
|
}
|
|
if v := mdutil.GetInt(md, "proxyProtocol"); v > 0 {
|
|
m["service.proxyProtocol"] = v
|
|
delete(m, "proxyProtocol")
|
|
}
|
|
if v := mdutil.GetString(md, "netns"); v != "" {
|
|
m["service.netns"] = v
|
|
delete(m, "netns")
|
|
}
|
|
if v := mdutil.GetString(md, "netns.out"); v != "" {
|
|
m["service.netns.out"] = v
|
|
delete(m, "netns.out")
|
|
}
|
|
|
|
selector := parseSelector(m)
|
|
|
|
serviceMd := map[string]any{}
|
|
handleMd := map[string]any{}
|
|
listenerMd := map[string]any{}
|
|
for k, v := range m {
|
|
if s, found := strings.CutPrefix(k, "service."); found {
|
|
serviceMd[s] = v
|
|
continue
|
|
}
|
|
if s, found := strings.CutPrefix(k, "handler."); found {
|
|
handleMd[s] = v
|
|
continue
|
|
}
|
|
if s, found := strings.CutPrefix(k, "listener."); found {
|
|
listenerMd[s] = v
|
|
continue
|
|
}
|
|
|
|
handleMd[k] = v
|
|
listenerMd[k] = v
|
|
serviceMd[k] = v
|
|
}
|
|
|
|
handlerCfg := &config.HandlerConfig{
|
|
Type: handler,
|
|
Auth: auth,
|
|
Metadata: handleMd,
|
|
}
|
|
listenerCfg := &config.ListenerConfig{
|
|
Type: listener,
|
|
TLS: tlsConfig,
|
|
Metadata: listenerMd,
|
|
}
|
|
if listenerCfg.Type == "ssh" || listenerCfg.Type == "sshd" {
|
|
handlerCfg.Auth = nil
|
|
listenerCfg.Auth = auth
|
|
}
|
|
|
|
for _, svc := range services {
|
|
if svc.Forwarder != nil {
|
|
svc.Forwarder.Selector = selector
|
|
}
|
|
svc.Handler = copyHandlerConfig(handlerCfg)
|
|
svc.Listener = copyListenerConfig(listenerCfg)
|
|
svc.Metadata = copyMap(serviceMd)
|
|
}
|
|
|
|
return services, nil
|
|
}
|
|
|
|
// buildNodeConfig converts a parsed node URL into a NodeConfig. Scheme
|
|
// components (connector+dialer) are resolved against the registry; path-based
|
|
// dialers use the URL path as the node address.
|
|
func buildNodeConfig(url *url.URL, m map[string]any) (*config.NodeConfig, error) {
|
|
var connector, dialer string
|
|
schemes := strings.Split(url.Scheme, "+")
|
|
if len(schemes) == 1 {
|
|
connector = schemes[0]
|
|
dialer = schemes[0]
|
|
}
|
|
if len(schemes) == 2 {
|
|
connector = schemes[0]
|
|
dialer = schemes[1]
|
|
}
|
|
|
|
md := mdx.NewMetadata(m)
|
|
|
|
if c := registry.ConnectorRegistry().Get(connector); c == nil {
|
|
connector = "http"
|
|
}
|
|
if d := registry.DialerRegistry().Get(dialer); d == nil {
|
|
dialer = "tcp"
|
|
if connector == "ssu" {
|
|
dialer = "udp"
|
|
}
|
|
}
|
|
|
|
var auth *config.AuthConfig
|
|
if url.User != nil {
|
|
if strings.HasPrefix(url.Scheme, "ss") {
|
|
auth = decodeSIP002Auth(url)
|
|
}
|
|
if auth == nil {
|
|
auth = &config.AuthConfig{
|
|
Username: url.User.Username(),
|
|
}
|
|
auth.Password, _ = url.User.Password()
|
|
}
|
|
}
|
|
|
|
if sauth := mdutil.GetString(md, "auth"); sauth != "" && auth == nil {
|
|
au, err := parseAuthFromCmd(sauth)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
auth = au
|
|
}
|
|
delete(m, "auth")
|
|
|
|
tlsConfig := &config.TLSConfig{
|
|
CertFile: mdutil.GetString(md, "tls.certFile", "certFile", "cert"),
|
|
KeyFile: mdutil.GetString(md, "tls.keyFile", "keyFile", "key"),
|
|
CAFile: mdutil.GetString(md, "tls.caFile", "caFile", "ca"),
|
|
Secure: mdutil.GetBool(md, "tls.secure", "secure"),
|
|
ServerName: mdutil.GetString(md, "tls.servername", "servername"),
|
|
}
|
|
if tlsConfig.ServerName == "" {
|
|
tlsConfig.ServerName = url.Hostname()
|
|
}
|
|
|
|
delete(m, "tls.certFile")
|
|
delete(m, "certFile")
|
|
delete(m, "cert")
|
|
delete(m, "tls.keyFile")
|
|
delete(m, "keyFile")
|
|
delete(m, "key")
|
|
delete(m, "tls.caFile")
|
|
delete(m, "caFile")
|
|
delete(m, "ca")
|
|
delete(m, "tls.secure")
|
|
delete(m, "secure")
|
|
delete(m, "tls.servername")
|
|
delete(m, "serverName")
|
|
|
|
if !tlsConfig.Secure && tlsConfig.CertFile == "" && tlsConfig.CAFile == "" && tlsConfig.ServerName == "" {
|
|
tlsConfig = nil
|
|
}
|
|
|
|
nodeMd := map[string]any{}
|
|
connectorMd := map[string]any{}
|
|
dialerMd := map[string]any{}
|
|
for k, v := range m {
|
|
if s, found := strings.CutPrefix(k, "node."); found {
|
|
nodeMd[s] = v
|
|
continue
|
|
}
|
|
if s, found := strings.CutPrefix(k, "connector."); found {
|
|
connectorMd[s] = v
|
|
continue
|
|
}
|
|
if s, found := strings.CutPrefix(k, "dialer."); found {
|
|
dialerMd[s] = v
|
|
continue
|
|
}
|
|
|
|
connectorMd[k] = v
|
|
dialerMd[k] = v
|
|
nodeMd[k] = v
|
|
}
|
|
|
|
// For path-based protocols (Unix socket dialers and serial), use path as address
|
|
var nodeAddr string
|
|
isPathBasedProtocol := dialer == "unix" || dialer == "serial"
|
|
if isPathBasedProtocol {
|
|
path := url.EscapedPath()
|
|
if url.Host != "" {
|
|
// Two slashes: host + path = relative path
|
|
nodeAddr = url.Host + strings.TrimPrefix(path, "/")
|
|
} else if path != "" {
|
|
// Three slashes: path is absolute (keep leading slash)
|
|
nodeAddr = path
|
|
} else {
|
|
nodeAddr = url.Host
|
|
}
|
|
} else {
|
|
nodeAddr = url.Host
|
|
}
|
|
|
|
node := &config.NodeConfig{
|
|
Addr: nodeAddr,
|
|
Metadata: nodeMd,
|
|
}
|
|
node.Connector = &config.ConnectorConfig{
|
|
Type: connector,
|
|
Auth: auth,
|
|
Metadata: connectorMd,
|
|
}
|
|
node.Dialer = &config.DialerConfig{
|
|
Type: dialer,
|
|
TLS: tlsConfig,
|
|
Metadata: dialerMd,
|
|
}
|
|
|
|
if node.Dialer.Type == "ssh" || node.Dialer.Type == "sshd" {
|
|
node.Connector.Auth = nil
|
|
node.Dialer.Auth = auth
|
|
}
|
|
|
|
return node, nil
|
|
}
|
|
|
|
// Norm normalizes a raw command-line string into a parsed URL. Missing schemes
|
|
// default to "auto" and "https" is rewritten to "http+tls". An empty string
|
|
// returns ErrInvalidCmd.
|
|
func Norm(s string) (*url.URL, error) {
|
|
s = strings.TrimSpace(s)
|
|
if s == "" {
|
|
return nil, ErrInvalidCmd
|
|
}
|
|
|
|
if s[0] == ':' || !strings.Contains(s, "://") {
|
|
s = "auto://" + s
|
|
}
|
|
|
|
url, err := url.Parse(s)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if url.Scheme == "https" {
|
|
url.Scheme = "http+tls"
|
|
}
|
|
|
|
return url, nil
|
|
}
|
|
|
|
// decodeSIP002Auth attempts to decode a Shadowsocks SIP002-style URL where
|
|
// the userinfo is a single base64-encoded "method:password" string with no
|
|
// separate password field. Returns nil if the URL does not use this format.
|
|
func decodeSIP002Auth(u *url.URL) *config.AuthConfig {
|
|
if u.User == nil {
|
|
return nil
|
|
}
|
|
_, hasPassword := u.User.Password()
|
|
if hasPassword {
|
|
return nil
|
|
}
|
|
username := u.User.Username()
|
|
if username == "" {
|
|
return nil
|
|
}
|
|
|
|
decoded, err := base64.RawURLEncoding.DecodeString(username)
|
|
if err != nil {
|
|
decoded, err = base64.StdEncoding.DecodeString(username)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
cs := string(decoded)
|
|
n := strings.IndexByte(cs, ':')
|
|
if n < 0 {
|
|
return nil
|
|
}
|
|
|
|
return &config.AuthConfig{
|
|
Username: cs[:n],
|
|
Password: cs[n+1:],
|
|
}
|
|
}
|
|
|
|
// parseAuthFromCmd decodes a base64-encoded "username:password" string from
|
|
// a query parameter into an AuthConfig. The password portion is optional.
|
|
func parseAuthFromCmd(sa string) (*config.AuthConfig, error) {
|
|
v, err := base64.RawURLEncoding.DecodeString(sa)
|
|
if err != nil {
|
|
v, err = base64.StdEncoding.DecodeString(sa)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
cs := string(v)
|
|
n := strings.IndexByte(cs, ':')
|
|
if n < 0 {
|
|
return &config.AuthConfig{
|
|
Username: cs,
|
|
}, nil
|
|
}
|
|
|
|
return &config.AuthConfig{
|
|
Username: cs[:n],
|
|
Password: cs[n+1:],
|
|
}, nil
|
|
}
|
|
|
|
// parseSelector extracts selector configuration (strategy, maxFails,
|
|
// failTimeout) from metadata and removes the consumed keys. Returns nil
|
|
// if no selector parameters are present.
|
|
func parseSelector(m map[string]any) *config.SelectorConfig {
|
|
md := mdx.NewMetadata(m)
|
|
strategy := mdutil.GetString(md, "strategy")
|
|
maxFails := mdutil.GetInt(md, "maxFails", "max_fails")
|
|
failTimeout := mdutil.GetDuration(md, "failTimeout", "fail_timeout")
|
|
if strategy == "" && maxFails <= 0 && failTimeout <= 0 {
|
|
return nil
|
|
}
|
|
if strategy == "" {
|
|
strategy = "round"
|
|
}
|
|
if maxFails <= 0 {
|
|
maxFails = 1
|
|
}
|
|
if failTimeout <= 0 {
|
|
failTimeout = 30 * time.Second
|
|
}
|
|
|
|
delete(m, "strategy")
|
|
delete(m, "maxFails")
|
|
delete(m, "max_fails")
|
|
delete(m, "failTimeout")
|
|
delete(m, "fail_timeout")
|
|
|
|
return &config.SelectorConfig{
|
|
Strategy: strategy,
|
|
MaxFails: maxFails,
|
|
FailTimeout: failTimeout,
|
|
}
|
|
}
|
|
|
|
func copyMap(m map[string]any) map[string]any {
|
|
if m == nil {
|
|
return nil
|
|
}
|
|
c := make(map[string]any, len(m))
|
|
for k, v := range m {
|
|
c[k] = v
|
|
}
|
|
return c
|
|
}
|
|
|
|
func copyAuth(cfg *config.AuthConfig) *config.AuthConfig {
|
|
if cfg == nil {
|
|
return nil
|
|
}
|
|
c := *cfg
|
|
return &c
|
|
}
|
|
|
|
func copyTLS(cfg *config.TLSConfig) *config.TLSConfig {
|
|
if cfg == nil {
|
|
return nil
|
|
}
|
|
c := *cfg
|
|
return &c
|
|
}
|
|
|
|
func copyConnectorConfig(cfg *config.ConnectorConfig) *config.ConnectorConfig {
|
|
if cfg == nil {
|
|
return nil
|
|
}
|
|
c := *cfg
|
|
c.Auth = copyAuth(cfg.Auth)
|
|
c.TLS = copyTLS(cfg.TLS)
|
|
c.Metadata = copyMap(cfg.Metadata)
|
|
return &c
|
|
}
|
|
|
|
func copyDialerConfig(cfg *config.DialerConfig) *config.DialerConfig {
|
|
if cfg == nil {
|
|
return nil
|
|
}
|
|
c := *cfg
|
|
c.Auth = copyAuth(cfg.Auth)
|
|
c.TLS = copyTLS(cfg.TLS)
|
|
c.Metadata = copyMap(cfg.Metadata)
|
|
return &c
|
|
}
|
|
|
|
func copyHandlerConfig(cfg *config.HandlerConfig) *config.HandlerConfig {
|
|
if cfg == nil {
|
|
return nil
|
|
}
|
|
c := *cfg
|
|
c.Auth = copyAuth(cfg.Auth)
|
|
c.TLS = copyTLS(cfg.TLS)
|
|
c.Metadata = copyMap(cfg.Metadata)
|
|
return &c
|
|
}
|
|
|
|
func copyListenerConfig(cfg *config.ListenerConfig) *config.ListenerConfig {
|
|
if cfg == nil {
|
|
return nil
|
|
}
|
|
c := *cfg
|
|
c.Auth = copyAuth(cfg.Auth)
|
|
c.TLS = copyTLS(cfg.TLS)
|
|
c.Metadata = copyMap(cfg.Metadata)
|
|
return &c
|
|
}
|