diff --git a/internal/web/service/access_log_parse_test.go b/internal/web/service/access_log_parse_test.go index 2d57da48e..be04c1833 100644 --- a/internal/web/service/access_log_parse_test.go +++ b/internal/web/service/access_log_parse_test.go @@ -1,6 +1,12 @@ package service -import "testing" +import ( + "encoding/json" + "testing" + + "github.com/mhsanaei/3x-ui/v3/internal/amneziawg" + "github.com/mhsanaei/3x-ui/v3/internal/database/model" +) func TestParseAccessLogFields(t *testing.T) { malformed := []string{ @@ -43,3 +49,38 @@ func TestParseAccessLogFields(t *testing.T) { t.Error("DateTime was not parsed from a well-formed line") } } + +func TestAmneziawgEmailIndex(t *testing.T) { + settings, err := json.Marshal(amneziawg.InboundSettings{ + Server: &amneziawg.ServerSettings{ + PrivateKey: "priv", + PublicKey: "pub", + SubnetIP: "10.8.1.0", + SubnetCIDR: 24, + }, + Clients: []model.Client{ + {Email: "router@x", Enable: true, PublicKey: "pubA", AllowedIPs: []string{"10.8.1.3/32"}}, + {Email: "disabled@x", Enable: false, PublicKey: "pubB", AllowedIPs: []string{"10.8.1.4/32"}}, + }, + }) + if err != nil { + t.Fatalf("marshal settings: %v", err) + } + + inbounds := []*model.Inbound{ + {Id: 10, Tag: "in-443-udp", Protocol: model.AmneziaWG, Port: 443, Settings: string(settings)}, + {Id: 11, Tag: "in-443-tcp", Protocol: model.VLESS, Port: 443, Settings: "{}"}, + } + + index := amneziawgEmailIndex(inbounds) + + if got := index["in-443-udp|10.8.1.3"]; got != "router@x" { + t.Errorf(`index["in-443-udp|10.8.1.3"] = %q, want "router@x"`, got) + } + if _, ok := index["in-443-udp|10.8.1.4"]; ok { + t.Error("a disabled peer must not appear in the index") + } + if len(index) != 1 { + t.Errorf("expected exactly 1 entry (non-amneziawg inbound and disabled peer excluded), got %d: %+v", len(index), index) + } +} diff --git a/internal/web/service/server.go b/internal/web/service/server.go index 7afa144ca..c661a51de 100644 --- a/internal/web/service/server.go +++ b/internal/web/service/server.go @@ -28,8 +28,10 @@ import ( "sync" "time" + "github.com/mhsanaei/3x-ui/v3/internal/amneziawg" "github.com/mhsanaei/3x-ui/v3/internal/config" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/logger" "github.com/mhsanaei/3x-ui/v3/internal/util/common" "github.com/mhsanaei/3x-ui/v3/internal/util/sys" @@ -1166,6 +1168,33 @@ func parseAccessLogFields(line string) LogEntry { return entry } +// amneziawgEmailIndex maps "|" to that peer's +// email, for every AmneziaWG inbound in inbounds. dokodemo-door (the +// TPROXY bridge every AmneziaWG peer's traffic is routed through, tagged +// with the AmneziaWG inbound's own tag) has no per-user identity, so Xray's +// access log never has an "email:" token for these lines -- only the +// decapsulated peer's own tunnel IP survives as the log's "from" address. +// This lets GetXrayLogs recover which client that was, the same way it's +// already shown for every authenticated protocol. +func amneziawgEmailIndex(inbounds []*model.Inbound) map[string]string { + index := make(map[string]string) + for _, ib := range inbounds { + instance, ok := amneziawg.InstanceFromInbound(ib) + if !ok { + continue + } + for _, peer := range instance.Peers { + if peer.Email == "" { + continue + } + if ip := amneziawg.FirstIPv4(peer.AllowedIPs); ip != "" { + index[ib.Tag+"|"+ip] = peer.Email + } + } + } + return index +} + func (s *ServerService) GetXrayLogs( count string, filter string, @@ -1184,6 +1213,11 @@ func (s *ServerService) GetXrayLogs( countInt, _ := strconv.Atoi(count) var entries []LogEntry + var amneziawgEmails map[string]string + if inbounds, err := s.inboundService.GetAllInbounds(); err == nil { + amneziawgEmails = amneziawgEmailIndex(inbounds) + } + pathToAccessLog, err := xray.GetAccessLogPath() if err != nil { return nil @@ -1212,6 +1246,14 @@ func (s *ServerService) GetXrayLogs( entry := parseAccessLogFields(line) + if entry.Email == "" && len(amneziawgEmails) > 0 { + if ip, _, splitErr := stdnet.SplitHostPort(entry.FromAddress); splitErr == nil { + if email, ok := amneziawgEmails[entry.Inbound+"|"+ip]; ok { + entry.Email = email + } + } + } + if logEntryContains(line, freedoms) { if showDirect == "false" { continue