175 lines
4.9 KiB
Go
175 lines
4.9 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
func parseFlags() (*Config, error) {
|
|
host := flag.String("host", "127.0.0.1", "Listen host")
|
|
port := flag.Int("port", 1443, "Listen port")
|
|
secret := flag.String("secret", "", "MTProto secret (32 hex chars)")
|
|
genSecret := flag.Bool("gen-secret", false, "Generate random secret and print it")
|
|
verbose := flag.Bool("v", false, "Verbose logs")
|
|
logFile := flag.String("log-file", "", "Log file path")
|
|
logMaxMB := flag.Float64("log-max-mb", 5, "Max log file size before rotate")
|
|
logBackups := flag.Int("log-backups", 0, "Number of rotated backups")
|
|
bufKB := flag.Int("buf-kb", 256, "Socket buffer size in KB")
|
|
poolSize := flag.Int("pool-size", 4, "WS pool size per DC")
|
|
cfproxyDomain := flag.String("cfproxy-domain", defaultCFProxyDomain, "Cloudflare-proxied domain for WS fallback")
|
|
noCfproxy := flag.Bool("no-cfproxy", false, "Disable Cloudflare proxy fallback")
|
|
cfproxyPriority := flag.Bool("cfproxy-priority", true, "Try cfproxy before TCP fallback")
|
|
maxConns := flag.Int("max-conns", defaultMaxConns, "Max concurrent client sessions")
|
|
dcIPDefault := flag.String("dc-ip-default", "149.154.167.220", "Default WS target IP for all implicit DCs when --dc-ip is not provided")
|
|
dcIPDefaultPool := flag.String("dc-ip-default-pool", "", "Default WS target IP pool for implicit DCs, comma-separated")
|
|
pprofListen := flag.String("pprof-listen", "", "Optional pprof listen address (e.g. 127.0.0.1:6060)")
|
|
|
|
var dcIPs multiFlag
|
|
var dcIPPools multiFlag
|
|
flag.Var(&dcIPs, "dc-ip", "Target DC IP as DC:IP; repeatable")
|
|
flag.Var(&dcIPPools, "dc-ip-pool", "Target pool as DC:IP1,IP2,...; repeatable")
|
|
flag.Parse()
|
|
|
|
if *secret == "" {
|
|
b := make([]byte, 16)
|
|
if _, err := rand.Read(b); err != nil {
|
|
return nil, err
|
|
}
|
|
*secret = hex.EncodeToString(b)
|
|
if !*genSecret {
|
|
log.Printf("INFO Generated secret: %s", *secret)
|
|
}
|
|
}
|
|
if len(*secret) != 32 {
|
|
return nil, errors.New("secret must be exactly 32 hex chars")
|
|
}
|
|
if _, err := hex.DecodeString(*secret); err != nil {
|
|
return nil, errors.New("secret must be valid hex")
|
|
}
|
|
|
|
defaultTargetIP := strings.TrimSpace(*dcIPDefault)
|
|
if net.ParseIP(defaultTargetIP) == nil {
|
|
return nil, fmt.Errorf("invalid --dc-ip-default: %s", defaultTargetIP)
|
|
}
|
|
|
|
defaultPool := []string{defaultTargetIP}
|
|
if strings.TrimSpace(*dcIPDefaultPool) != "" {
|
|
poolIPs, err := parseIPCSV(*dcIPDefaultPool)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid --dc-ip-default-pool: %w", err)
|
|
}
|
|
defaultPool = poolIPs
|
|
}
|
|
|
|
dcMap := map[int]string{}
|
|
dcPool := map[int][]string{}
|
|
for _, dc := range []int{2, 4} {
|
|
dcPool[dc] = append([]string(nil), defaultPool...)
|
|
dcMap[dc] = defaultPool[0]
|
|
}
|
|
|
|
for _, item := range dcIPs {
|
|
parts := strings.SplitN(item, ":", 2)
|
|
if len(parts) != 2 {
|
|
return nil, fmt.Errorf("invalid --dc-ip: %s", item)
|
|
}
|
|
dc, err := strconv.Atoi(parts[0])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid dc: %s", parts[0])
|
|
}
|
|
if net.ParseIP(parts[1]) == nil {
|
|
return nil, fmt.Errorf("invalid ip: %s", parts[1])
|
|
}
|
|
ip := parts[1]
|
|
dcPool[dc] = []string{ip}
|
|
dcMap[dc] = ip
|
|
}
|
|
|
|
for _, item := range dcIPPools {
|
|
parts := strings.SplitN(item, ":", 2)
|
|
if len(parts) != 2 {
|
|
return nil, fmt.Errorf("invalid --dc-ip-pool: %s", item)
|
|
}
|
|
dc, err := strconv.Atoi(parts[0])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid dc in --dc-ip-pool: %s", parts[0])
|
|
}
|
|
poolIPs, err := parseIPCSV(parts[1])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid --dc-ip-pool for dc %d: %w", dc, err)
|
|
}
|
|
dcPool[dc] = append([]string(nil), poolIPs...)
|
|
dcMap[dc] = dcPool[dc][0]
|
|
}
|
|
|
|
return &Config{
|
|
Host: *host,
|
|
Port: *port,
|
|
SecretHex: *secret,
|
|
GenSecret: *genSecret,
|
|
DCMap: dcMap,
|
|
DCPool: dcPool,
|
|
FallbackCFProxy: !*noCfproxy,
|
|
FallbackCFProxyPriority: *cfproxyPriority,
|
|
FallbackCFProxyDomain: strings.TrimSpace(*cfproxyDomain),
|
|
Verbose: *verbose,
|
|
BufKB: maxInt(*bufKB, 4),
|
|
PoolSize: maxInt(*poolSize, 0),
|
|
MaxConns: maxInt(*maxConns, 1),
|
|
LogFile: *logFile,
|
|
LogMaxMB: *logMaxMB,
|
|
LogBackups: maxInt(*logBackups, 0),
|
|
PprofListen: strings.TrimSpace(*pprofListen),
|
|
}, nil
|
|
}
|
|
|
|
type multiFlag []string
|
|
|
|
func (m *multiFlag) String() string { return strings.Join(*m, ",") }
|
|
func (m *multiFlag) Set(v string) error {
|
|
*m = append(*m, v)
|
|
return nil
|
|
}
|
|
|
|
func maxInt(a, b int) int {
|
|
if a > b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
func parseIPCSV(raw string) ([]string, error) {
|
|
parts := strings.Split(raw, ",")
|
|
out := make([]string, 0, len(parts))
|
|
for _, p := range parts {
|
|
ip := strings.TrimSpace(p)
|
|
if ip == "" {
|
|
continue
|
|
}
|
|
if net.ParseIP(ip) == nil {
|
|
return nil, fmt.Errorf("invalid ip: %s", ip)
|
|
}
|
|
out = appendUniqueIP(out, ip)
|
|
}
|
|
if len(out) == 0 {
|
|
return nil, errors.New("empty ip pool")
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func appendUniqueIP(dst []string, ip string) []string {
|
|
for _, v := range dst {
|
|
if v == ip {
|
|
return dst
|
|
}
|
|
}
|
|
return append(dst, ip)
|
|
}
|