mirror of
https://github.com/DanielLavrushin/b4.git
synced 2026-07-09 16:00:05 +00:00
fix: resolve device duplication and missing names on Traffic page (#240)
Some checks failed
Deploy documentation / build-and-deploy (push) Has been cancelled
Some checks failed
Deploy documentation / build-and-deploy (push) Has been cancelled
This commit is contained in:
parent
3e72216a68
commit
90fd37eebb
18 changed files with 362 additions and 129 deletions
|
|
@ -4,6 +4,8 @@
|
|||
|
||||
- ADDED: **Block (blackhole) routing mode** - a new "Block" option in a set's Routing tab that blocks all matched traffic (ad/tracker domains, IPs, or a GeoSite category like `category-ads-all`) across the whole network - every LAN device and the router itself - with no output interface needed. It blocks by name and not just by IP, so it keeps working even with encrypted DNS and won't break unrelated sites sharing the same servers. See [Blocking](https://daniellavrushin.github.io/b4/docs/sets/blocking) for setup and details.
|
||||
- ADDED: **Upstream SOCKS5 proxy now routes UDP too** - sets that send traffic to an upstream SOCKS5 proxy used to forward only regular (TCP) connections; UDP traffic - such as QUIC (used by YouTube and many Google and video services) and DNS - bypassed the proxy and went out directly. UDP from LAN devices now goes through the upstream proxy as well, so a set can route a device's full traffic through it. The proxy must accept UDP.
|
||||
- FIXED: **A device could appear twice on the Traffic page, and hand-added names were missing** - in the Aggregated view's Devices sidebar, one device sometimes showed up as two: once under its name and once under its bare IP address, because some of its connections arrived without a recognizable hardware address. Those connections are now matched back to the right device by its IP, so each device appears once. Devices you add by hand (Settings -> Devices), including ones behind another router, now show their assigned name here too instead of a raw IP.
|
||||
- FIXED: **Command-line options like `--skip-tables` got saved into the config and stuck** - starting b4 with an extra command-line option used to write that option into the configuration file, so it kept applying on later starts even when you ran b4 without it. Command-line options now affect only the run you pass them to and are never saved into the config.
|
||||
|
||||
## [1.64.0] - 2026-06-01
|
||||
|
||||
|
|
|
|||
|
|
@ -2,28 +2,157 @@ package config
|
|||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
func (c *Config) BindFlags(cmd *cobra.Command) {
|
||||
// Config path
|
||||
type CLIOverrides struct {
|
||||
QueueNum int
|
||||
Threads int
|
||||
Mark uint
|
||||
IPv4Enabled bool
|
||||
IPv6Enabled bool
|
||||
MonitorInterval int
|
||||
SkipSetup bool
|
||||
Masquerade bool
|
||||
MasqueradeInterface string
|
||||
Instaflush bool
|
||||
Syslog bool
|
||||
ErrorFile string
|
||||
WebPort int
|
||||
}
|
||||
|
||||
func (c *Config) BindFlags(cmd *cobra.Command, o *CLIOverrides) {
|
||||
cmd.Flags().StringVar(&c.ConfigPath, "config", c.ConfigPath, "Path to config file")
|
||||
|
||||
// Queue configuration
|
||||
cmd.Flags().IntVar(&c.Queue.StartNum, "queue-num", c.Queue.StartNum, "Netfilter queue number")
|
||||
cmd.Flags().IntVar(&c.Queue.Threads, "threads", c.Queue.Threads, "Number of worker threads")
|
||||
cmd.Flags().UintVar(&c.Queue.Mark, "mark", c.Queue.Mark, "Packet mark value (default 32768)")
|
||||
cmd.Flags().BoolVar(&c.Queue.IPv4Enabled, "ipv4", c.Queue.IPv4Enabled, "Enable IPv4 processing")
|
||||
cmd.Flags().BoolVar(&c.Queue.IPv6Enabled, "ipv6", c.Queue.IPv6Enabled, "Enable IPv6 processing")
|
||||
|
||||
// System configuration
|
||||
cmd.Flags().IntVar(&c.System.Tables.MonitorInterval, "tables-monitor-interval", c.System.Tables.MonitorInterval, "Tables monitor interval in seconds (default 10, 0 to disable)")
|
||||
cmd.Flags().BoolVar(&c.System.Tables.SkipSetup, "skip-tables", c.System.Tables.SkipSetup, "Skip iptables/nftables setup on startup")
|
||||
cmd.Flags().BoolVar(&c.System.Tables.Masquerade, "masquerade", c.System.Tables.Masquerade, "Enable NAT masquerade (useful for containers)")
|
||||
cmd.Flags().StringVar(&c.System.Tables.MasqueradeInterface, "masquerade-interface", c.System.Tables.MasqueradeInterface, "Restrict masquerade to this output interface (empty = all)")
|
||||
|
||||
// Logging configuration
|
||||
cmd.Flags().BoolVarP(&c.System.Logging.Instaflush, "instaflush", "i", c.System.Logging.Instaflush, "Flush logs immediately")
|
||||
cmd.Flags().BoolVar(&c.System.Logging.Syslog, "syslog", c.System.Logging.Syslog, "Enable syslog output")
|
||||
cmd.Flags().StringVar(&c.System.Logging.ErrorFile, "error-file", c.System.Logging.ErrorFile, "Path to error log file (empty disables)")
|
||||
|
||||
// Web Server configuration
|
||||
cmd.Flags().IntVar(&c.System.WebServer.Port, "web-port", c.System.WebServer.Port, "Port for internal web server (0 disables)")
|
||||
d := DefaultConfig
|
||||
cmd.Flags().IntVar(&o.QueueNum, "queue-num", d.Queue.StartNum, "Netfilter queue number")
|
||||
cmd.Flags().IntVar(&o.Threads, "threads", d.Queue.Threads, "Number of worker threads")
|
||||
cmd.Flags().UintVar(&o.Mark, "mark", d.Queue.Mark, "Packet mark value (default 32768)")
|
||||
cmd.Flags().BoolVar(&o.IPv4Enabled, "ipv4", d.Queue.IPv4Enabled, "Enable IPv4 processing")
|
||||
cmd.Flags().BoolVar(&o.IPv6Enabled, "ipv6", d.Queue.IPv6Enabled, "Enable IPv6 processing")
|
||||
cmd.Flags().IntVar(&o.MonitorInterval, "tables-monitor-interval", d.System.Tables.MonitorInterval, "Tables monitor interval in seconds (default 10, 0 to disable)")
|
||||
cmd.Flags().BoolVar(&o.SkipSetup, "skip-tables", d.System.Tables.SkipSetup, "Skip iptables/nftables setup on startup")
|
||||
cmd.Flags().BoolVar(&o.Masquerade, "masquerade", d.System.Tables.Masquerade, "Enable NAT masquerade (useful for containers)")
|
||||
cmd.Flags().StringVar(&o.MasqueradeInterface, "masquerade-interface", d.System.Tables.MasqueradeInterface, "Restrict masquerade to this output interface (empty = all)")
|
||||
cmd.Flags().BoolVarP(&o.Instaflush, "instaflush", "i", d.System.Logging.Instaflush, "Flush logs immediately")
|
||||
cmd.Flags().BoolVar(&o.Syslog, "syslog", d.System.Logging.Syslog, "Enable syslog output")
|
||||
cmd.Flags().StringVar(&o.ErrorFile, "error-file", d.System.Logging.ErrorFile, "Path to error log file (empty disables)")
|
||||
cmd.Flags().IntVar(&o.WebPort, "web-port", d.System.WebServer.Port, "Port for internal web server (0 disables)")
|
||||
}
|
||||
|
||||
var persistedOverrides struct {
|
||||
snapshot *Config
|
||||
fields map[string]bool
|
||||
overrides CLIOverrides
|
||||
}
|
||||
|
||||
func (c *Config) ApplyCLIOverrides(cmd *cobra.Command, o *CLIOverrides) {
|
||||
fields := make(map[string]bool)
|
||||
changed := func(name string) bool {
|
||||
if cmd.Flags().Changed(name) {
|
||||
fields[name] = true
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
snapshot := &Config{Queue: c.Queue, System: c.System}
|
||||
|
||||
if changed("queue-num") {
|
||||
c.Queue.StartNum = o.QueueNum
|
||||
}
|
||||
if changed("threads") {
|
||||
c.Queue.Threads = o.Threads
|
||||
}
|
||||
if changed("mark") {
|
||||
c.Queue.Mark = o.Mark
|
||||
}
|
||||
if changed("ipv4") {
|
||||
c.Queue.IPv4Enabled = o.IPv4Enabled
|
||||
}
|
||||
if changed("ipv6") {
|
||||
c.Queue.IPv6Enabled = o.IPv6Enabled
|
||||
}
|
||||
if changed("tables-monitor-interval") {
|
||||
c.System.Tables.MonitorInterval = o.MonitorInterval
|
||||
}
|
||||
if changed("skip-tables") {
|
||||
c.System.Tables.SkipSetup = o.SkipSetup
|
||||
}
|
||||
if changed("masquerade") {
|
||||
c.System.Tables.Masquerade = o.Masquerade
|
||||
}
|
||||
if changed("masquerade-interface") {
|
||||
c.System.Tables.MasqueradeInterface = o.MasqueradeInterface
|
||||
}
|
||||
if changed("instaflush") {
|
||||
c.System.Logging.Instaflush = o.Instaflush
|
||||
}
|
||||
if changed("syslog") {
|
||||
c.System.Logging.Syslog = o.Syslog
|
||||
}
|
||||
if changed("error-file") {
|
||||
c.System.Logging.ErrorFile = o.ErrorFile
|
||||
}
|
||||
if changed("web-port") {
|
||||
c.System.WebServer.Port = o.WebPort
|
||||
}
|
||||
|
||||
if len(fields) > 0 {
|
||||
persistedOverrides.snapshot = snapshot
|
||||
persistedOverrides.fields = fields
|
||||
persistedOverrides.overrides = *o
|
||||
} else {
|
||||
persistedOverrides.snapshot = nil
|
||||
persistedOverrides.fields = nil
|
||||
persistedOverrides.overrides = CLIOverrides{}
|
||||
}
|
||||
}
|
||||
|
||||
func stripCLIOverrides(c *Config) *Config {
|
||||
snap := persistedOverrides.snapshot
|
||||
f := persistedOverrides.fields
|
||||
if snap == nil || len(f) == 0 {
|
||||
return c
|
||||
}
|
||||
ov := persistedOverrides.overrides
|
||||
|
||||
clone := *c
|
||||
if f["queue-num"] && c.Queue.StartNum == ov.QueueNum {
|
||||
clone.Queue.StartNum = snap.Queue.StartNum
|
||||
}
|
||||
if f["threads"] && c.Queue.Threads == ov.Threads {
|
||||
clone.Queue.Threads = snap.Queue.Threads
|
||||
}
|
||||
if f["mark"] && c.Queue.Mark == ov.Mark {
|
||||
clone.Queue.Mark = snap.Queue.Mark
|
||||
}
|
||||
if f["ipv4"] && c.Queue.IPv4Enabled == ov.IPv4Enabled {
|
||||
clone.Queue.IPv4Enabled = snap.Queue.IPv4Enabled
|
||||
}
|
||||
if f["ipv6"] && c.Queue.IPv6Enabled == ov.IPv6Enabled {
|
||||
clone.Queue.IPv6Enabled = snap.Queue.IPv6Enabled
|
||||
}
|
||||
if f["tables-monitor-interval"] && c.System.Tables.MonitorInterval == ov.MonitorInterval {
|
||||
clone.System.Tables.MonitorInterval = snap.System.Tables.MonitorInterval
|
||||
}
|
||||
if f["skip-tables"] && c.System.Tables.SkipSetup == ov.SkipSetup {
|
||||
clone.System.Tables.SkipSetup = snap.System.Tables.SkipSetup
|
||||
}
|
||||
if f["masquerade"] && c.System.Tables.Masquerade == ov.Masquerade {
|
||||
clone.System.Tables.Masquerade = snap.System.Tables.Masquerade
|
||||
}
|
||||
if f["masquerade-interface"] && c.System.Tables.MasqueradeInterface == ov.MasqueradeInterface {
|
||||
clone.System.Tables.MasqueradeInterface = snap.System.Tables.MasqueradeInterface
|
||||
}
|
||||
if f["instaflush"] && c.System.Logging.Instaflush == ov.Instaflush {
|
||||
clone.System.Logging.Instaflush = snap.System.Logging.Instaflush
|
||||
}
|
||||
if f["syslog"] && c.System.Logging.Syslog == ov.Syslog {
|
||||
clone.System.Logging.Syslog = snap.System.Logging.Syslog
|
||||
}
|
||||
if f["error-file"] && c.System.Logging.ErrorFile == ov.ErrorFile {
|
||||
clone.System.Logging.ErrorFile = snap.System.Logging.ErrorFile
|
||||
}
|
||||
if f["web-port"] && c.System.WebServer.Port == ov.WebPort {
|
||||
clone.System.WebServer.Port = snap.System.WebServer.Port
|
||||
}
|
||||
return &clone
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ func (c *Config) SaveToFile(path string) error {
|
|||
|
||||
c.Version = CurrentConfigVersion
|
||||
|
||||
data, err := MarshalSparse(c)
|
||||
data, err := MarshalSparse(stripCLIOverrides(c))
|
||||
if err != nil {
|
||||
return log.Errorf("failed to marshal config: %v", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1087,7 +1087,7 @@ func TestEscalateTo_RoundtripAndDefault(t *testing.T) {
|
|||
}
|
||||
|
||||
loaded := NewConfig()
|
||||
if err := loaded.LoadWithMigration(path); err != nil {
|
||||
if _, err := loaded.LoadWithMigration(path); err != nil {
|
||||
t.Fatalf("LoadWithMigration: %v", err)
|
||||
}
|
||||
if got := loaded.GetSetById("a"); got == nil || got.Escalate.To != "b" {
|
||||
|
|
|
|||
|
|
@ -641,40 +641,38 @@ func discoverConfigPath() string {
|
|||
return "/etc/b4/b4.json"
|
||||
}
|
||||
|
||||
func (c *Config) LoadWithMigration(path string) error {
|
||||
discovered := false
|
||||
func (c *Config) LoadWithMigration(path string) (bool, error) {
|
||||
if path == "" {
|
||||
path = discoverConfigPath()
|
||||
c.ConfigPath = path
|
||||
discovered = true
|
||||
log.Infof("Using config path: %s", path)
|
||||
}
|
||||
c.ConfigPath = path
|
||||
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
if discovered && os.IsNotExist(err) {
|
||||
if os.IsNotExist(err) {
|
||||
log.Infof("Config file does not exist yet, using defaults: %s", path)
|
||||
return nil
|
||||
return true, nil
|
||||
}
|
||||
return log.Errorf("failed to stat config file: %v", err)
|
||||
return false, log.Errorf("failed to stat config file: %v", err)
|
||||
}
|
||||
if info.IsDir() {
|
||||
return log.Errorf("config path is a directory, not a file: %s", path)
|
||||
return false, log.Errorf("config path is a directory, not a file: %s", path)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return log.Errorf("failed to read config file: %v", err)
|
||||
return false, log.Errorf("failed to read config file: %v", err)
|
||||
}
|
||||
|
||||
var rawJSON map[string]interface{}
|
||||
if err := json.Unmarshal(data, &rawJSON); err != nil {
|
||||
return log.Errorf("failed to parse config file: %v", err)
|
||||
return false, log.Errorf("failed to parse config file: %v", err)
|
||||
}
|
||||
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return log.Errorf("failed to parse config file: %v", err)
|
||||
return false, log.Errorf("failed to parse config file: %v", err)
|
||||
}
|
||||
|
||||
rawSets := raw["sets"]
|
||||
|
|
@ -682,35 +680,37 @@ func (c *Config) LoadWithMigration(path string) error {
|
|||
|
||||
withoutSets, _ := json.Marshal(raw)
|
||||
if err := json.Unmarshal(withoutSets, c); err != nil {
|
||||
return log.Errorf("failed to parse config file: %v", err)
|
||||
return false, log.Errorf("failed to parse config file: %v", err)
|
||||
}
|
||||
|
||||
if rawSets != nil {
|
||||
var setArray []json.RawMessage
|
||||
if err := json.Unmarshal(rawSets, &setArray); err != nil {
|
||||
return log.Errorf("failed to parse sets: %v", err)
|
||||
return false, log.Errorf("failed to parse sets: %v", err)
|
||||
}
|
||||
c.Sets = make([]*SetConfig, 0, len(setArray))
|
||||
for _, rs := range setArray {
|
||||
set := NewSetConfig()
|
||||
if err := json.Unmarshal(rs, &set); err != nil {
|
||||
return log.Errorf("failed to parse set: %v", err)
|
||||
return false, log.Errorf("failed to parse set: %v", err)
|
||||
}
|
||||
c.Sets = append(c.Sets, &set)
|
||||
}
|
||||
}
|
||||
|
||||
migrated := false
|
||||
if c.Version < CurrentConfigVersion {
|
||||
migrated = true
|
||||
log.Infof("Config version %d is older than current version %d, migrating",
|
||||
c.Version, CurrentConfigVersion)
|
||||
if err := c.applyMigrations(c.Version, rawJSON); err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
c.System.Geo.SanitizePaths(filepath.Dir(c.ConfigPath))
|
||||
|
||||
return nil
|
||||
return migrated, nil
|
||||
}
|
||||
|
||||
func (c *Config) applyMigrations(startVersion int, rawJSON map[string]interface{}) error {
|
||||
|
|
|
|||
|
|
@ -46,22 +46,25 @@ func TestLoadWithMigration(t *testing.T) {
|
|||
|
||||
t.Run("empty path with no default config returns nil", func(t *testing.T) {
|
||||
cfg := NewConfig()
|
||||
if err := cfg.LoadWithMigration(""); err != nil {
|
||||
if _, err := cfg.LoadWithMigration(""); err != nil {
|
||||
t.Errorf("expected nil for empty path with no discoverable config: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("nonexistent file errors", func(t *testing.T) {
|
||||
t.Run("nonexistent file signals creation", func(t *testing.T) {
|
||||
cfg := NewConfig()
|
||||
err := cfg.LoadWithMigration(filepath.Join(tmpDir, "nope.json"))
|
||||
if err == nil {
|
||||
t.Error("expected error for nonexistent file")
|
||||
needsSave, err := cfg.LoadWithMigration(filepath.Join(tmpDir, "nope.json"))
|
||||
if err != nil {
|
||||
t.Errorf("expected no error for nonexistent file, got %v", err)
|
||||
}
|
||||
if !needsSave {
|
||||
t.Error("expected needsSave=true for nonexistent file")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("directory path errors", func(t *testing.T) {
|
||||
cfg := NewConfig()
|
||||
err := cfg.LoadWithMigration(tmpDir)
|
||||
_, err := cfg.LoadWithMigration(tmpDir)
|
||||
if err == nil {
|
||||
t.Error("expected error for directory path")
|
||||
}
|
||||
|
|
@ -78,7 +81,7 @@ func TestLoadWithMigration(t *testing.T) {
|
|||
os.WriteFile(path, []byte(v0Json), 0644)
|
||||
|
||||
cfg := NewConfig()
|
||||
if err := cfg.LoadWithMigration(path); err != nil {
|
||||
if _, err := cfg.LoadWithMigration(path); err != nil {
|
||||
t.Fatalf("LoadWithMigration failed: %v", err)
|
||||
}
|
||||
|
||||
|
|
@ -97,7 +100,7 @@ func TestLoadWithMigration(t *testing.T) {
|
|||
cfg.SaveToFile(path)
|
||||
|
||||
loaded := NewConfig()
|
||||
if err := loaded.LoadWithMigration(path); err != nil {
|
||||
if _, err := loaded.LoadWithMigration(path); err != nil {
|
||||
t.Fatalf("LoadWithMigration failed: %v", err)
|
||||
}
|
||||
if loaded.Version != CurrentConfigVersion {
|
||||
|
|
@ -115,7 +118,7 @@ func TestLoadWithMigration(t *testing.T) {
|
|||
cfg.SaveToFile(path)
|
||||
|
||||
loaded := NewConfig()
|
||||
if err := loaded.LoadWithMigration(path); err != nil {
|
||||
if _, err := loaded.LoadWithMigration(path); err != nil {
|
||||
t.Fatalf("LoadWithMigration failed: %v", err)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -253,6 +253,8 @@ type MTProtoConfig struct {
|
|||
CFProxyEnabled bool `json:"cfproxy_enabled"` // enable Cloudflare-proxied fallback WS domains (rescues DCs the network blocks)
|
||||
CFProxyURL string `json:"cfproxy_url"` // URL to refresh CF-proxy domain list; empty = built-in default
|
||||
CFWorkerDomain string `json:"cfworker_domain"` // user's Cloudflare Worker domain(s) (workers.dev), comma-separated; free per-user WS relay tried before the shared CF pool
|
||||
|
||||
BridgeSkipNativeEdge bool `json:"-"`
|
||||
}
|
||||
|
||||
type Socks5Config struct {
|
||||
|
|
|
|||
|
|
@ -105,6 +105,10 @@ export function ConnectionsPage() {
|
|||
const [ipInfoToken, setIpInfoToken] = useState<string>("");
|
||||
const [devicesEnabled, setDevicesEnabled] = useState<boolean>(false);
|
||||
const [deviceMap, setDeviceMap] = useState<Record<string, string>>({});
|
||||
const [ipToMac, setIpToMac] = useState<Record<string, string>>({});
|
||||
const [configIpToMac, setConfigIpToMac] = useState<Record<string, string>>(
|
||||
{},
|
||||
);
|
||||
const [configDeviceNames, setConfigDeviceNames] = useState<
|
||||
Record<string, string>
|
||||
>({});
|
||||
|
|
@ -122,23 +126,30 @@ export function ConnectionsPage() {
|
|||
useEffect(() => {
|
||||
if (!devicesEnabled) {
|
||||
setDeviceMap({ ...configDeviceNames });
|
||||
setIpToMac({ ...configIpToMac });
|
||||
return;
|
||||
}
|
||||
devicesApi
|
||||
.list()
|
||||
.then((data) => {
|
||||
const map: Record<string, string> = {};
|
||||
const ipMap: Record<string, string> = {};
|
||||
for (const d of data.devices || []) {
|
||||
const normalized = d.mac.toUpperCase().replaceAll("-", ":");
|
||||
map[normalized] = d.alias || d.vendor || "";
|
||||
if (d.ip) ipMap[d.ip] = normalized;
|
||||
}
|
||||
for (const [mac, name] of Object.entries(configDeviceNames)) {
|
||||
map[mac] = name;
|
||||
}
|
||||
for (const [ip, mac] of Object.entries(configIpToMac)) {
|
||||
ipMap[ip] = mac;
|
||||
}
|
||||
setDeviceMap(map);
|
||||
setIpToMac(ipMap);
|
||||
})
|
||||
.catch(() => {});
|
||||
}, [devicesEnabled, configDeviceNames]);
|
||||
}, [devicesEnabled, configDeviceNames, configIpToMac]);
|
||||
|
||||
const fetchSets = useCallback(async (signal?: AbortSignal) => {
|
||||
try {
|
||||
|
|
@ -157,12 +168,18 @@ export function ConnectionsPage() {
|
|||
false,
|
||||
);
|
||||
const names: Record<string, string> = {};
|
||||
const configIps: Record<string, string> = {};
|
||||
for (const d of data.queue?.devices?.devices || []) {
|
||||
if (d.mac && d.name) {
|
||||
names[d.mac.toUpperCase().replaceAll("-", ":")] = d.name;
|
||||
const normalized = d.mac?.toUpperCase().replaceAll("-", ":");
|
||||
if (normalized && d.name) {
|
||||
names[normalized] = d.name;
|
||||
}
|
||||
if (normalized && d.ip) {
|
||||
configIps[d.ip] = normalized;
|
||||
}
|
||||
}
|
||||
setConfigDeviceNames(names);
|
||||
setConfigIpToMac(configIps);
|
||||
}
|
||||
} catch (error) {
|
||||
if ((error as Error).name !== "AbortError") {
|
||||
|
|
@ -359,6 +376,7 @@ export function ConnectionsPage() {
|
|||
<AggregatedView
|
||||
lines={domains}
|
||||
deviceMap={deviceMap}
|
||||
ipToMac={ipToMac}
|
||||
paused={pauseDomains}
|
||||
onTogglePause={() => setPauseDomains(!pauseDomains)}
|
||||
showAll={showAll}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { useTranslation } from "react-i18next";
|
|||
interface Props {
|
||||
lines: string[];
|
||||
deviceMap: Record<string, string>;
|
||||
ipToMac: Record<string, string>;
|
||||
paused: boolean;
|
||||
onTogglePause: () => void;
|
||||
showAll: boolean;
|
||||
|
|
@ -67,6 +68,7 @@ const getGroupSearchableValues = (g: EnrichedGroup): (string | null)[] => [
|
|||
export const AggregatedView = ({
|
||||
lines,
|
||||
deviceMap,
|
||||
ipToMac,
|
||||
paused,
|
||||
onTogglePause,
|
||||
showAll,
|
||||
|
|
@ -94,7 +96,7 @@ export const AggregatedView = ({
|
|||
localStorage.setItem("b4_connections_sidebar_collapsed", sidebarCollapsed ? "1" : "0");
|
||||
}, [sidebarCollapsed]);
|
||||
|
||||
const state = useConnectionGroups(lines, deviceMap, paused);
|
||||
const state = useConnectionGroups(lines, deviceMap, paused, ipToMac);
|
||||
|
||||
useEffect(() => {
|
||||
const id = globalThis.setInterval(() => setNowTick(Date.now()), 1000);
|
||||
|
|
|
|||
|
|
@ -38,9 +38,14 @@ export function useConnectionGroups(
|
|||
lines: string[],
|
||||
deviceMap: Record<string, string>,
|
||||
paused: boolean,
|
||||
ipToMac: Record<string, string> = {},
|
||||
): GroupsState {
|
||||
const workerRef = useRef<Worker | null>(null);
|
||||
const lastSentinelRef = useRef<string | null>(null);
|
||||
const linesRef = useRef<string[]>(lines);
|
||||
const pausedRef = useRef<boolean>(paused);
|
||||
linesRef.current = lines;
|
||||
pausedRef.current = paused;
|
||||
const [snapshot, setSnapshot] = useState<AggregatorSnapshot | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -58,6 +63,22 @@ export function useConnectionGroups(
|
|||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const w = workerRef.current;
|
||||
if (!w) return;
|
||||
const setMsg: AggregatorInput = { type: "setIpToMac", map: ipToMac };
|
||||
w.postMessage(setMsg);
|
||||
if (pausedRef.current) return;
|
||||
const clearMsg: AggregatorInput = { type: "clear" };
|
||||
w.postMessage(clearMsg);
|
||||
const all = linesRef.current;
|
||||
if (all.length > 0) {
|
||||
const ingestMsg: AggregatorInput = { type: "ingest", lines: all };
|
||||
w.postMessage(ingestMsg);
|
||||
}
|
||||
lastSentinelRef.current = all.at(-1) ?? null;
|
||||
}, [ipToMac]);
|
||||
|
||||
useEffect(() => {
|
||||
const w = workerRef.current;
|
||||
if (!w) return;
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ let snapshotInterval = 500;
|
|||
|
||||
const groups = new Map<string, ConnectionGroup>();
|
||||
const devices = new Map<string, DeviceSummary>();
|
||||
let ipToMac: Record<string, string> = {};
|
||||
let totalPackets = 0;
|
||||
let unmatchedCount = 0;
|
||||
let dirty = false;
|
||||
|
|
@ -118,9 +119,11 @@ function ingest(lines: string[]): void {
|
|||
const p = parseLine(line);
|
||||
if (!p) continue;
|
||||
|
||||
const mac = p.sourceAlias
|
||||
? normalizeMac(p.sourceAlias)
|
||||
: stripPort(p.source);
|
||||
let mac = p.sourceAlias ? normalizeMac(p.sourceAlias) : "";
|
||||
if (!mac) {
|
||||
const ip = stripPort(p.source);
|
||||
mac = ipToMac[ip] || ip;
|
||||
}
|
||||
const groupIdent = p.domain || p.destination || "?";
|
||||
const key = `${mac}|${p.protocol}|${groupIdent}`;
|
||||
|
||||
|
|
@ -250,6 +253,9 @@ ctx.onmessage = (e: MessageEvent<AggregatorInput>) => {
|
|||
bucketCount = Math.max(10, Math.min(300, msg.count));
|
||||
clearState();
|
||||
break;
|
||||
case "setIpToMac":
|
||||
ipToMac = msg.map;
|
||||
break;
|
||||
case "flush":
|
||||
dirty = true;
|
||||
postSnapshot();
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ export type AggregatorInput =
|
|||
| { type: "clear" }
|
||||
| { type: "setSnapshotInterval"; ms: number }
|
||||
| { type: "setBucketCount"; count: number }
|
||||
| { type: "setIpToMac"; map: Record<string, string> }
|
||||
| { type: "flush" };
|
||||
|
||||
export type AggregatorOutput = { type: "snapshot"; payload: AggregatorSnapshot };
|
||||
|
|
|
|||
10
src/main.go
10
src/main.go
|
|
@ -37,6 +37,7 @@ import (
|
|||
|
||||
var (
|
||||
cfg = config.NewConfig()
|
||||
cliOverrides config.CLIOverrides
|
||||
verboseFlag string
|
||||
showVersion bool
|
||||
clearTables bool
|
||||
|
|
@ -57,7 +58,7 @@ var rootCmd = &cobra.Command{
|
|||
|
||||
func init() {
|
||||
// Bind all configuration flags
|
||||
cfg.BindFlags(rootCmd)
|
||||
cfg.BindFlags(rootCmd, &cliOverrides)
|
||||
|
||||
// Add verbosity flags separately since they need special handling
|
||||
rootCmd.Flags().StringVar(&verboseFlag, "verbose", "info", "Set verbosity level (debug, trace, info, silent), default: info")
|
||||
|
|
@ -97,8 +98,11 @@ func runB4(cmd *cobra.Command, args []string) error {
|
|||
|
||||
initTimezone()
|
||||
|
||||
cfg.LoadWithMigration(cfg.ConfigPath)
|
||||
cfg.SaveToFile(cfg.ConfigPath)
|
||||
needsSave, _ := cfg.LoadWithMigration(cfg.ConfigPath)
|
||||
if needsSave {
|
||||
cfg.SaveToFile(cfg.ConfigPath)
|
||||
}
|
||||
cfg.ApplyCLIOverrides(cmd, &cliOverrides)
|
||||
|
||||
if cfg.System.Timezone != "" {
|
||||
config.ApplyTimezone(cfg.System.Timezone)
|
||||
|
|
|
|||
21
src/mtproto/logtag.go
Normal file
21
src/mtproto/logtag.go
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
package mtproto
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
const logTag = "tg-bridge"
|
||||
|
||||
var connSeq atomic.Uint64
|
||||
|
||||
func nextConnID() string {
|
||||
return strconv.FormatUint(connSeq.Add(1), 36)
|
||||
}
|
||||
|
||||
func tg(id string) string {
|
||||
if id == "" {
|
||||
return "[" + logTag + "]"
|
||||
}
|
||||
return "[" + logTag + " c=" + id + "]"
|
||||
}
|
||||
|
|
@ -36,7 +36,6 @@ func wsEdgeServesDC(absDC int) bool {
|
|||
return wsEdgeServedDCs[absDC]
|
||||
}
|
||||
|
||||
// workerDomains parses the comma-separated CF Worker domain list from config.
|
||||
func workerDomains(cfg *config.MTProtoConfig) []string {
|
||||
raw := strings.TrimSpace(cfg.CFWorkerDomain)
|
||||
if raw == "" {
|
||||
|
|
@ -51,8 +50,6 @@ func workerDomains(cfg *config.MTProtoConfig) []string {
|
|||
return out
|
||||
}
|
||||
|
||||
// workerDstIP returns the canonical public DC IP the worker should TCP-connect to
|
||||
// (the worker's ?dst= target). Mirrors tg-ws-proxy DC_DEFAULT_IPS.
|
||||
func workerDstIP(absDC int) string {
|
||||
addr, ok := dcAddressesV4[absDC]
|
||||
if !ok {
|
||||
|
|
@ -169,9 +166,9 @@ type transportPlan struct {
|
|||
sni string
|
||||
dialHost string
|
||||
dc int
|
||||
cfBase string // CF-proxy base domain (without "kwsN."); set => pin in balancer on success
|
||||
wsPath string // WS request path; "" defaults to /apiws. CF Worker uses /apiws?dst=&dc=
|
||||
isWorker bool // CF Worker plan: dedicated per-user relay, reaches any DC via ?dst=
|
||||
cfBase string
|
||||
wsPath string
|
||||
isWorker bool
|
||||
}
|
||||
|
||||
func (p transportPlan) describe() string {
|
||||
|
|
@ -227,7 +224,7 @@ func planTransports(cfg *config.MTProtoConfig, queueCfg config.QueueConfig, dc i
|
|||
if edgeIP == "" {
|
||||
edgeIP = telegramWSEdgeIP
|
||||
}
|
||||
if wsEdgeServesDC(absDC) {
|
||||
if wsEdgeServesDC(absDC) && !cfg.BridgeSkipNativeEdge {
|
||||
primary := transportPlan{kind: transportWS, dc: dc, sni: fmt.Sprintf("kws%d.web.telegram.org", absDC), dialHost: edgeIP}
|
||||
media := transportPlan{kind: transportWS, dc: dc, sni: fmt.Sprintf("kws%d-1.web.telegram.org", absDC), dialHost: edgeIP}
|
||||
if dc < 0 {
|
||||
|
|
@ -236,11 +233,6 @@ func planTransports(cfg *config.MTProtoConfig, queueCfg config.QueueConfig, dc i
|
|||
plans = append(plans, primary, media)
|
||||
}
|
||||
}
|
||||
// CF Worker (free per-user workers.dev relay). Tried after TG's own edge so
|
||||
// the fast native path wins for DC2/4, but before the shared CF pool so DCs
|
||||
// without a native edge (1/3/5) and throttled cases use the dedicated worker
|
||||
// instead of the rate-limited shared domains. The worker reaches any DC via
|
||||
// ?dst=<canonical DC IP>&dc=<n> (matches tg-ws-proxy CfWorker mode).
|
||||
if dst := workerDstIP(absDC); dst != "" {
|
||||
for _, wd := range workerDomains(cfg) {
|
||||
plans = append(plans, transportPlan{
|
||||
|
|
@ -261,8 +253,6 @@ func planTransports(cfg *config.MTProtoConfig, queueCfg config.QueueConfig, dc i
|
|||
cfBase: d,
|
||||
})
|
||||
}
|
||||
// CF-proxy fallback pool (matches tg-ws-proxy). Tried after TG's own edge so
|
||||
// the fast path wins when it works; CF rescues DCs the network blocks (esp. DC 1).
|
||||
if cfg.CFProxyEnabled {
|
||||
for _, base := range cfBalancerInst.domainsForDC(dc) {
|
||||
plans = append(plans, transportPlan{
|
||||
|
|
@ -290,22 +280,23 @@ func planTransports(cfg *config.MTProtoConfig, queueCfg config.QueueConfig, dc i
|
|||
}
|
||||
|
||||
func DialObfuscatedDC(cfg *config.MTProtoConfig, queueCfg config.QueueConfig, dc int, protoTag uint32) (*ObfuscatedConn, string, error) {
|
||||
return DialObfuscatedDCWithPool(cfg, queueCfg, dc, protoTag, nil)
|
||||
return DialObfuscatedDCWithPool(cfg, queueCfg, dc, protoTag, nil, "")
|
||||
}
|
||||
|
||||
func DialObfuscatedDCWithPool(cfg *config.MTProtoConfig, queueCfg config.QueueConfig, dc int, protoTag uint32, pool *wsPool) (*ObfuscatedConn, string, error) {
|
||||
func DialObfuscatedDCWithPool(cfg *config.MTProtoConfig, queueCfg config.QueueConfig, dc int, protoTag uint32, pool *wsPool, logID string) (*ObfuscatedConn, string, error) {
|
||||
tag := tg(logID)
|
||||
if pool != nil && !wsIsBlacklisted(dc) {
|
||||
if raw := pool.get(dc); raw != nil {
|
||||
obf, err := completeObfuscation(raw, dc, protoTag)
|
||||
if err == nil && raw.liveNow() {
|
||||
log.Infof("MTProto DC %d connected via ws-pool", dc)
|
||||
log.Infof("%s DC %d connected via ws-pool", tag, dc)
|
||||
wsRecordSuccess(dc)
|
||||
return obf, "ws-pool", nil
|
||||
}
|
||||
if err != nil {
|
||||
log.Debugf("MTProto DC %d pool conn obf init failed: %v", dc, err)
|
||||
log.Debugf("%s DC %d pool conn obf init failed: %v", tag, dc, err)
|
||||
} else {
|
||||
log.Debugf("MTProto DC %d pool conn died before relay; re-dialing fresh", dc)
|
||||
log.Debugf("%s DC %d pool conn died before relay; re-dialing fresh", tag, dc)
|
||||
}
|
||||
_ = raw.Close()
|
||||
}
|
||||
|
|
@ -325,7 +316,7 @@ func DialObfuscatedDCWithPool(cfg *config.MTProtoConfig, queueCfg config.QueueCo
|
|||
wsTried := 0
|
||||
wsRedirects := 0
|
||||
for _, p := range plans {
|
||||
log.Debugf("MTProto DC %d dialing %s", dc, p.describe())
|
||||
log.Debugf("%s DC %d dialing %s", tag, dc, p.describe())
|
||||
start := time.Now()
|
||||
var conn net.Conn
|
||||
var derr error
|
||||
|
|
@ -347,29 +338,27 @@ func DialObfuscatedDCWithPool(cfg *config.MTProtoConfig, queueCfg config.QueueCo
|
|||
} else if isDialTimeout(derr) {
|
||||
tcpRecordFailure(p.addr)
|
||||
}
|
||||
log.Debugf("MTProto DC %d %s failed after %dms: %v", dc, p.describe(), time.Since(start).Milliseconds(), derr)
|
||||
log.Debugf("%s DC %d %s failed after %dms: %v", tag, dc, p.describe(), time.Since(start).Milliseconds(), derr)
|
||||
continue
|
||||
}
|
||||
obfConn, oerr := completeObfuscation(conn, dc, protoTag)
|
||||
if oerr != nil {
|
||||
attempts = append(attempts, fmt.Sprintf("%s: %s", p.describe(), shortErr(oerr)))
|
||||
conn.Close()
|
||||
log.Debugf("MTProto DC %d obf init failed on %s: %v", dc, p.describe(), oerr)
|
||||
log.Debugf("%s DC %d obf init failed on %s: %v", tag, dc, p.describe(), oerr)
|
||||
continue
|
||||
}
|
||||
if p.kind == transportWS {
|
||||
wsRecordSuccess(dc)
|
||||
// pin successful CF-proxy domain for this DC so subsequent connections
|
||||
// try it first (mirrors tg-ws-proxy/proxy/balancer.py:update_domain_for_dc)
|
||||
if p.cfBase != "" {
|
||||
if cfBalancerInst.pin(dc, p.cfBase) {
|
||||
log.Infof("MTProto DC %d switched active CF domain to %s", dc, p.cfBase)
|
||||
log.Infof("%s DC %d switched active CF domain to %s", tag, dc, p.cfBase)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tcpRecordSuccess(p.addr)
|
||||
}
|
||||
log.Infof("MTProto DC %d connected via %s in %dms", dc, p.describe(), time.Since(start).Milliseconds())
|
||||
log.Infof("%s DC %d connected via %s in %dms", tag, dc, p.describe(), time.Since(start).Milliseconds())
|
||||
return obfConn, p.describe(), nil
|
||||
}
|
||||
|
||||
|
|
@ -543,10 +532,6 @@ func completeObfuscation(conn net.Conn, dc int, protoTag uint32) (*ObfuscatedCon
|
|||
encrypted := make([]byte, obfuscatedFrameLen)
|
||||
copy(encrypted, frame)
|
||||
encStream.XORKeyStream(encrypted, encrypted)
|
||||
// Restore bytes 0:56 to plaintext - only bytes 56:64 (proto_tag||dc||rnd) are
|
||||
// sent encrypted. tg-ws-proxy keeps the SKIP region (0:8) as plaintext too;
|
||||
// b4 was only restoring 8:56, leaving 0:7 as ciphertext on the wire. Some TG
|
||||
// endpoints inspect the SKIP bytes before decryption.
|
||||
copy(encrypted[0:56], frame[0:56])
|
||||
|
||||
if _, err := conn.Write(encrypted); err != nil {
|
||||
|
|
@ -560,24 +545,16 @@ func completeObfuscation(conn net.Conn, dc int, protoTag uint32) (*ObfuscatedCon
|
|||
}, nil
|
||||
}
|
||||
|
||||
// reservedFirst4Words are the first-4-byte little-endian values an obfuscated
|
||||
// frame must never start with: they collide with TLS/HTTP and the obfuscation
|
||||
// transport tags, so TG treats a connection beginning with one of them as a
|
||||
// different protocol. generateFrame avoids producing them; reservedFirst4
|
||||
// (transparent bridge) uses them to detect a non-obfuscated transport. Keep the
|
||||
// two in sync via this single list.
|
||||
var reservedFirst4Words = []uint32{
|
||||
0x44414548, // "HEAD"
|
||||
0x54534f50, // "POST"
|
||||
0x20544547, // "GET "
|
||||
0x4954504f, // "OPTI"
|
||||
0x02010316, // TLS record header
|
||||
0xdddddddd, // padded intermediate tag
|
||||
0xeeeeeeee, // intermediate tag
|
||||
0x44414548,
|
||||
0x54534f50,
|
||||
0x20544547,
|
||||
0x4954504f,
|
||||
0x02010316,
|
||||
0xdddddddd,
|
||||
0xeeeeeeee,
|
||||
}
|
||||
|
||||
// isReservedFirst4 reports whether the first 4 bytes are a reserved value
|
||||
// (0xef abridged-tag first byte, or any reservedFirst4Words value).
|
||||
func isReservedFirst4(b []byte) bool {
|
||||
if b[0] == 0xef {
|
||||
return true
|
||||
|
|
|
|||
|
|
@ -233,11 +233,13 @@ func (s *Server) acceptLoop(ln net.Listener) {
|
|||
|
||||
func (s *Server) handleConn(raw net.Conn) {
|
||||
clientAddr := raw.RemoteAddr().String()
|
||||
log.Infof("MTProto new connection from %s", clientAddr)
|
||||
id := nextConnID()
|
||||
tag := tg(id)
|
||||
log.Infof("%s proxy new connection from %s", tag, clientAddr)
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Errorf("MTProto panic from %s: %v", clientAddr, r)
|
||||
log.Errorf("%s proxy panic from %s: %v", tag, clientAddr, r)
|
||||
}
|
||||
}()
|
||||
|
||||
|
|
@ -253,42 +255,41 @@ func (s *Server) handleConn(raw net.Conn) {
|
|||
|
||||
tlsConn, err := AcceptFakeTLS(raw, secret)
|
||||
if err != nil {
|
||||
log.Debugf("MTProto fake-TLS failed from %s: %v", clientAddr, err)
|
||||
log.Debugf("%s proxy fake-TLS failed from %s: %v", tag, clientAddr, err)
|
||||
var vErr *FakeTLSVerifyError
|
||||
if errors.As(err, &vErr) && cfg.System.MTProto.FakeSNI != "" {
|
||||
proxyToMaskingDomain(raw, vErr.Initial, cfg.System.MTProto.FakeSNI, cfg.Queue.Mark)
|
||||
}
|
||||
return
|
||||
}
|
||||
log.Debugf("MTProto fake-TLS handshake OK from %s", clientAddr)
|
||||
log.Debugf("%s proxy fake-TLS handshake OK from %s", tag, clientAddr)
|
||||
|
||||
result, err := AcceptObfuscated(tlsConn, secret)
|
||||
if err != nil {
|
||||
log.Tracef("MTProto obfuscated2 failed from %s: %v", clientAddr, err)
|
||||
log.Tracef("%s proxy obfuscated2 failed from %s: %v", tag, clientAddr, err)
|
||||
return
|
||||
}
|
||||
log.Debugf("MTProto client from %s wants DC %d proto=0x%08x", clientAddr, result.DC, result.ProtoTag)
|
||||
log.Debugf("%s proxy client from %s wants DC %d proto=0x%08x", tag, clientAddr, result.DC, result.ProtoTag)
|
||||
_ = raw.SetDeadline(time.Time{})
|
||||
|
||||
dcConn, transport, err := DialObfuscatedDCWithPool(&cfg.System.MTProto, cfg.Queue, result.DC, result.ProtoTag, s.wsPool.Load())
|
||||
dcConn, transport, err := DialObfuscatedDCWithPool(&cfg.System.MTProto, cfg.Queue, result.DC, result.ProtoTag, s.wsPool.Load(), id)
|
||||
if err != nil {
|
||||
// throttle ERROR-level spam from a permanently-broken DC; full detail still goes to Debug
|
||||
if shouldLogDialError(result.DC) {
|
||||
log.Errorf("MTProto dial DC %d: %v", result.DC, err)
|
||||
log.Errorf("%s proxy dial DC %d failed: %v", tag, result.DC, err)
|
||||
} else {
|
||||
log.Debugf("MTProto dial DC %d (suppressed): %v", result.DC, err)
|
||||
log.Debugf("%s proxy dial DC %d failed (suppressed): %v", tag, result.DC, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
defer dcConn.Close()
|
||||
|
||||
log.Infof("MTProto relay: %s <-> DC%d (%s)", clientAddr, result.DC, transport)
|
||||
log.Infof("%s proxy relay %s <-> DC%d via %s", tag, clientAddr, result.DC, transport)
|
||||
|
||||
var splitter *msgSplitter
|
||||
if _, ok := dcConn.Conn.(*wsConn); ok {
|
||||
splitter = newMsgSplitter(result.ProtoTag)
|
||||
}
|
||||
s.relay(result.Conn, dcConn, splitter, fmt.Sprintf("%s<->DC%d", clientAddr, result.DC))
|
||||
s.relay(result.Conn, dcConn, splitter, fmt.Sprintf("%s %s<->DC%d", tag, clientAddr, result.DC))
|
||||
}
|
||||
|
||||
func (s *Server) relay(client, dc io.ReadWriteCloser, splitter *msgSplitter, label string) {
|
||||
|
|
@ -321,7 +322,7 @@ func relayConns(client, dc io.ReadWriteCloser, splitter *msgSplitter, label stri
|
|||
}
|
||||
}
|
||||
counter.Store(total)
|
||||
log.Debugf("MTProto relay %s %s: %d bytes, err=%v", label, dir, total, err)
|
||||
log.Debugf("%s %s: %d bytes, err=%v", label, dir, total, err)
|
||||
errCh <- err
|
||||
}
|
||||
|
||||
|
|
@ -351,7 +352,7 @@ func relayConns(client, dc io.ReadWriteCloser, splitter *msgSplitter, label stri
|
|||
}
|
||||
}
|
||||
counter.Store(total)
|
||||
log.Debugf("MTProto relay %s %s: %d bytes, err=%v", label, dir, total, err)
|
||||
log.Debugf("%s %s: %d bytes, err=%v", label, dir, total, err)
|
||||
errCh <- err
|
||||
}
|
||||
|
||||
|
|
@ -366,5 +367,5 @@ func relayConns(client, dc io.ReadWriteCloser, splitter *msgSplitter, label stri
|
|||
_ = client.Close()
|
||||
_ = dc.Close()
|
||||
<-errCh
|
||||
log.Infof("MTProto session %s closed: up=%d down=%d in %dms", label, upBytes.Load(), downBytes.Load(), time.Since(start).Milliseconds())
|
||||
log.Infof("%s closed: up=%d down=%d in %dms", label, upBytes.Load(), downBytes.Load(), time.Since(start).Milliseconds())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,64 +91,75 @@ func (b *TransparentBridge) getPool() *wsPool {
|
|||
}
|
||||
|
||||
func (b *TransparentBridge) Handle(client net.Conn, origIP net.IP, origPort int) (bool, net.Conn) {
|
||||
id := nextConnID()
|
||||
tag := tg(id)
|
||||
log.Tracef("%s bridge accept %s -> %s:%d", tag, client.RemoteAddr(), origIP, origPort)
|
||||
_ = client.SetReadDeadline(time.Now().Add(5 * time.Second))
|
||||
init := make([]byte, obfuscatedFrameLen)
|
||||
head, herr := io.ReadFull(client, init[:4])
|
||||
if herr != nil {
|
||||
_ = client.SetReadDeadline(time.Time{})
|
||||
if head == 0 {
|
||||
log.Tracef("%s bridge empty conn from %s -> drop", tag, origIP)
|
||||
return true, nil
|
||||
}
|
||||
log.Debugf("%s bridge short head (%d B) from %s:%d -> fail open", tag, head, origIP, origPort)
|
||||
return false, &prefixConn{Conn: client, prefix: append([]byte(nil), init[:head]...)}
|
||||
}
|
||||
if reservedFirst4(init[:4]) {
|
||||
_ = client.SetReadDeadline(time.Time{})
|
||||
log.Debugf("MTProto transparent: non-obfuscated transport (% x) from %s -> fail open", init[:4], origIP)
|
||||
log.Debugf("%s bridge non-obfuscated transport (% x) from %s:%d -> fail open", tag, init[:4], origIP, origPort)
|
||||
return false, &prefixConn{Conn: client, prefix: append([]byte(nil), init[:4]...)}
|
||||
}
|
||||
n, rerr := io.ReadFull(client, init[4:])
|
||||
_ = client.SetReadDeadline(time.Time{})
|
||||
if rerr != nil {
|
||||
log.Debugf("%s bridge short handshake (%d/%d B) from %s:%d -> fail open", tag, 4+n, obfuscatedFrameLen, origIP, origPort)
|
||||
return false, &prefixConn{Conn: client, prefix: append([]byte(nil), init[:4+n]...)}
|
||||
}
|
||||
|
||||
res, derr := decodeObfuscatedDirect(init, client)
|
||||
if derr != nil {
|
||||
log.Debugf("MTProto transparent: obfuscated decode failed from %s: %v -> fail open", origIP, derr)
|
||||
log.Debugf("%s bridge obfuscated decode failed from %s:%d: %v -> fail open", tag, origIP, origPort, derr)
|
||||
return false, &prefixConn{Conn: client, prefix: append([]byte(nil), init...)}
|
||||
}
|
||||
log.Tracef("%s bridge handshake ok from %s:%d: proto=0x%08x handshake-dc=%d", tag, origIP, origPort, res.ProtoTag, res.DC)
|
||||
|
||||
var dc int
|
||||
var dcSrc string
|
||||
if mapped, ok := dcForIP(origIP); ok {
|
||||
dc, dcSrc = mapped, "ip"
|
||||
} else if mapped, ok := dcForIPRange(origIP); ok {
|
||||
dc, dcSrc = mapped, "ip-range"
|
||||
} else if validTransparentDC(res.DC) {
|
||||
dc, dcSrc = res.DC, "handshake"
|
||||
} else if mapped, ok := dcForIPRange(origIP); ok {
|
||||
dc, dcSrc = mapped, "ip-range"
|
||||
} else {
|
||||
log.Debugf("MTProto transparent: unresolved DC for %s (handshake dc=%d proto=0x%08x) -> fail open", origIP, res.DC, res.ProtoTag)
|
||||
log.Debugf("%s bridge unresolved DC for %s:%d (handshake dc=%d proto=0x%08x) -> fail open", tag, origIP, origPort, res.DC, res.ProtoTag)
|
||||
return false, &prefixConn{Conn: client, prefix: append([]byte(nil), init...)}
|
||||
}
|
||||
if rng, ok := dcForIPRange(origIP); ok && validTransparentDC(res.DC) && rng != res.DC {
|
||||
log.Debugf("%s bridge DC ambiguity for %s: ip-range=DC%d handshake=DC%d -> using DC%d (src=%s)", tag, origIP, rng, res.DC, dc, dcSrc)
|
||||
}
|
||||
|
||||
cfg := b.cfg.Load()
|
||||
mtCfg := cfg.System.MTProto
|
||||
mtCfg.UpstreamMode = "auto"
|
||||
mtCfg.DCRelay = ""
|
||||
mtCfg.BridgeSkipNativeEdge = true
|
||||
|
||||
dcConn, transport, err := DialObfuscatedDCWithPool(&mtCfg, cfg.Queue, dc, res.ProtoTag, b.getPool())
|
||||
dcConn, transport, err := DialObfuscatedDCWithPool(&mtCfg, cfg.Queue, dc, res.ProtoTag, nil, id)
|
||||
if err != nil {
|
||||
if shouldLogDialError(dc) {
|
||||
log.Errorf("MTProto transparent dial DC %d: %v", dc, err)
|
||||
log.Errorf("%s bridge dial DC %d failed: %v", tag, dc, err)
|
||||
} else {
|
||||
log.Debugf("MTProto transparent dial DC %d (suppressed): %v", dc, err)
|
||||
log.Debugf("%s bridge dial DC %d failed (suppressed): %v", tag, dc, err)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
defer dcConn.Close()
|
||||
|
||||
label := fmt.Sprintf("%s<->DC%d(transparent)", client.RemoteAddr(), dc)
|
||||
log.Infof("MTProto transparent relay: %s -> DC%d (%s) [dc-from=%s]", origIP, dc, transport, dcSrc)
|
||||
label := fmt.Sprintf("%s %s<->DC%d(transparent)", tag, client.RemoteAddr(), dc)
|
||||
log.Infof("%s bridge relay %s:%d -> DC%d via %s [dc-from=%s]", tag, origIP, origPort, dc, transport, dcSrc)
|
||||
|
||||
var splitter *msgSplitter
|
||||
if _, isWS := dcConn.Conn.(*wsConn); isWS {
|
||||
|
|
@ -158,6 +169,37 @@ func (b *TransparentBridge) Handle(client net.Conn, origIP net.IP, origPort int)
|
|||
return true, nil
|
||||
}
|
||||
|
||||
func (b *TransparentBridge) FailOpenViaWorker(client net.Conn, origIP net.IP, origPort int) bool {
|
||||
cfg := b.cfg.Load()
|
||||
mt := cfg.System.MTProto
|
||||
domains := workerDomains(&mt)
|
||||
if len(domains) == 0 {
|
||||
return false
|
||||
}
|
||||
id := nextConnID()
|
||||
tag := tg(id)
|
||||
dst := origIP.String()
|
||||
dc := 0
|
||||
if m, ok := dcForIP(origIP); ok {
|
||||
dc = m
|
||||
} else if m, ok := dcForIPRange(origIP); ok {
|
||||
dc = m
|
||||
}
|
||||
for _, wd := range domains {
|
||||
path := fmt.Sprintf("/apiws?dst=%s&dc=%d&port=%d", dst, dc, origPort)
|
||||
wc, derr := dialWS(wd, wd, path, wsDialTimeout, cfg.Queue.Mark)
|
||||
if derr != nil {
|
||||
log.Debugf("%s failopen worker dial %s for %s:%d failed: %v", tag, wd, dst, origPort, derr)
|
||||
continue
|
||||
}
|
||||
log.Infof("%s failopen relay %s:%d via wsworker://%s", tag, dst, origPort, wd)
|
||||
label := fmt.Sprintf("%s %s<->%s:%d(failopen)", tag, client.RemoteAddr(), dst, origPort)
|
||||
relayConns(client, wc, nil, label, &b.bufPool)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func validTransparentDC(dc int) bool {
|
||||
a := dc
|
||||
if a < 0 {
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ type DomainResolver interface {
|
|||
|
||||
type MTProtoBridge interface {
|
||||
Handle(client net.Conn, origIP net.IP, origPort int) (bool, net.Conn)
|
||||
FailOpenViaWorker(client net.Conn, origIP net.IP, origPort int) bool
|
||||
}
|
||||
|
||||
type Listener struct {
|
||||
|
|
@ -201,6 +202,9 @@ func (l *Listener) handle(client net.Conn) {
|
|||
} else if failover != nil {
|
||||
client = failover
|
||||
}
|
||||
if l.Bridge.FailOpenViaWorker(client, origIP, origPort) {
|
||||
return
|
||||
}
|
||||
}
|
||||
l.failOpenDirect(client, origIP, origPort)
|
||||
return
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue