mirror of
https://github.com/SunBK201/UA3F.git
synced 2025-12-16 16:57:08 +00:00
feat: add connection statistics tracking
This commit is contained in:
parent
a392e6272a
commit
a44f772c99
1
.gitignore
vendored
1
.gitignore
vendored
@ -25,3 +25,4 @@ bin/
|
||||
dist/
|
||||
ipkg/etc/
|
||||
ipkg/usr/
|
||||
test/
|
||||
@ -23,6 +23,18 @@ if pass_stats_file then
|
||||
pass_stats_file:close()
|
||||
end
|
||||
|
||||
local conn_stats = {}
|
||||
local conn_stats_file = io.open("/var/log/ua3f/conn_stats", "r")
|
||||
if conn_stats_file then
|
||||
for line in conn_stats_file:lines() do
|
||||
local protocol, srcAddr, destAddr, duration = line:match("^(%S+)%s(%S+)%s(%S+)%s(.+)$")
|
||||
if protocol and srcAddr and destAddr and duration then
|
||||
table.insert(conn_stats, {protocol = protocol, srcAddr = srcAddr, destAddr = destAddr, duration = duration})
|
||||
end
|
||||
end
|
||||
conn_stats_file:close()
|
||||
end
|
||||
|
||||
local function rowstyle(i)
|
||||
return (i % 2 == 0) and "cbi-rowstyle-2" or "cbi-rowstyle-1"
|
||||
end
|
||||
@ -30,13 +42,13 @@ end
|
||||
|
||||
<h3><%:Statistics%></h3>
|
||||
|
||||
<div class="cbi-section-descr"><%:User-Agent Rewrite Statistics%></div>
|
||||
<div class="cbi-section-descr" style="font-weight:bold;"><%:User-Agent Rewrite Statistics%></div>
|
||||
<table id="rewrite-stats-table" class="table cbi-section-table">
|
||||
<tr class="tr table-titles">
|
||||
<th class="th"><%:Host%></th>
|
||||
<th class="th"><%:Rewrite Count%></th>
|
||||
<th class="th"><%:Original User-Agent%></th>
|
||||
<th class="th"><%:Modified User-Agent%></th>
|
||||
<th class="th" data-sortable-row="true"><%:Host%></th>
|
||||
<th class="th" data-sortable-row="true"><%:Rewrite Count%></th>
|
||||
<th class="th" data-sortable-row="true"><%:Original User-Agent%></th>
|
||||
<th class="th" data-sortable-row="true"><%:Modified User-Agent%></th>
|
||||
</tr>
|
||||
|
||||
<% for i, item in ipairs(rewrite_stats) do %>
|
||||
@ -49,13 +61,13 @@ end
|
||||
<% end %>
|
||||
</table>
|
||||
|
||||
<div class="cbi-section-descr"><%:User-Agent Pass-Through Statistics%></div>
|
||||
<div class="cbi-section-descr" style="font-weight:bold;"><%:User-Agent Pass-Through Statistics%></div>
|
||||
<table id="pass-stats-table" class="table cbi-section-table">
|
||||
<tr class="tr table-titles">
|
||||
<th class="th"><%:User-Agent%></th>
|
||||
<th class="th"><%:Pass-Through Count%></th>
|
||||
<th class="th"><%:Last Source Address%></th>
|
||||
<th class="th"><%:Last Destination Address%></th>
|
||||
<th class="th" data-sortable-row="true"><%:User-Agent%></th>
|
||||
<th class="th" data-sortable-row="true"><%:Pass-Through Count%></th>
|
||||
<th class="th" data-sortable-row="true"><%:Last Source Address%></th>
|
||||
<th class="th" data-sortable-row="true"><%:Last Destination Address%></th>
|
||||
</tr>
|
||||
|
||||
<% for i, item in ipairs(pass_stats) do %>
|
||||
@ -68,6 +80,30 @@ end
|
||||
<% end %>
|
||||
</table>
|
||||
|
||||
<div class="cbi-section-descr" style="font-weight:bold;"><%:Connection Statistics%></div>
|
||||
<table id="conn-stats-table" class="table cbi-section-table">
|
||||
<tr>
|
||||
<td colspan="4" style="padding:5px 0;">
|
||||
<%:Total Connections%>: <%= #conn_stats %>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="tr table-titles">
|
||||
<th class="th" data-sortable-row="true"><%:Protocol%></th>
|
||||
<th class="th" data-sortable-row="true"><%:Source Address%></th>
|
||||
<th class="th" data-sortable-row="true"><%:Destination Address%></th>
|
||||
<th class="th" data-sortable-row="true"><%:Duration%></th>
|
||||
</tr>
|
||||
|
||||
<% for i, item in ipairs(conn_stats) do %>
|
||||
<tr class="tr <%= rowstyle(i) %>">
|
||||
<td class="td" data-title="<%:Protocol%>"><span><%= item.protocol %></span></td>
|
||||
<td class="td" data-title="<%:Source Address%>"><span><%= item.srcAddr %></span></td>
|
||||
<td class="td" data-title="<%:Destination Address%>"><span><%= item.destAddr %></span></td>
|
||||
<td class="td" data-title="<%:Duration%>"><span><%= item.duration %></span></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</table>
|
||||
|
||||
|
||||
<script type="text/javascript">
|
||||
async function updateStats() {
|
||||
@ -79,6 +115,7 @@ end
|
||||
const doc = parser.parseFromString(text, "text/html");
|
||||
const newRewriteTable = doc.querySelector("#rewrite-stats-table");
|
||||
const newPassTable = doc.querySelector("#pass-stats-table");
|
||||
const newConnTable = doc.querySelector("#conn-stats-table");
|
||||
const newLog = doc.querySelector("#cbid\\.ua3f\\.main\\.log");
|
||||
|
||||
if (newRewriteTable) {
|
||||
@ -87,6 +124,9 @@ end
|
||||
if (newPassTable) {
|
||||
document.querySelector("#pass-stats-table").innerHTML = newPassTable.innerHTML;
|
||||
}
|
||||
if (newConnTable) {
|
||||
document.querySelector("#conn-stats-table").innerHTML = newConnTable.innerHTML;
|
||||
}
|
||||
if (newLog) {
|
||||
document.querySelector("#cbid\\.ua3f\\.main\\.log").value = newLog.value;
|
||||
}
|
||||
@ -140,4 +180,25 @@ end
|
||||
#pass-stats-table td:nth-child(4) {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
#conn-stats-table th:nth-child(1),
|
||||
#conn-stats-table td:nth-child(1) {
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
#conn-stats-table th:nth-child(2),
|
||||
#conn-stats-table td:nth-child(2) {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
#conn-stats-table th:nth-child(3),
|
||||
#conn-stats-table td:nth-child(3) {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
#conn-stats-table th:nth-child(4),
|
||||
#conn-stats-table td:nth-child(4) {
|
||||
width: 15%;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -109,3 +109,21 @@ msgstr "固定 TTL"
|
||||
|
||||
msgid "Set the TTL 64 for packets"
|
||||
msgstr "固定数据包的 TTL"
|
||||
|
||||
msgid "Connection Statistics"
|
||||
msgstr "连接实时统计"
|
||||
|
||||
msgid "Protocol"
|
||||
msgstr "嗅探协议"
|
||||
|
||||
msgid "Source Address"
|
||||
msgstr "源地址"
|
||||
|
||||
msgid "Destination Address"
|
||||
msgstr "目标地址"
|
||||
|
||||
msgid "Duration"
|
||||
msgstr "持续时间"
|
||||
|
||||
msgid "Total Connections"
|
||||
msgstr "连接总数"
|
||||
@ -160,6 +160,11 @@ func (r *Rewriter) Process(dst net.Conn, src net.Conn, destAddr string, srcAddr
|
||||
if strings.HasSuffix(destAddr, "443") && sniff.SniffTLSClientHello(reader) {
|
||||
r.Cache.Add(destAddr, struct{}{})
|
||||
log.LogInfoWithAddr(srcAddr, destAddr, "tls client hello detected, added to cache")
|
||||
statistics.AddConnection(&statistics.ConnectionRecord{
|
||||
Protocol: sniff.HTTPS,
|
||||
SrcAddr: srcAddr,
|
||||
DestAddr: destAddr,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@ -172,14 +177,34 @@ func (r *Rewriter) Process(dst net.Conn, src net.Conn, destAddr string, srcAddr
|
||||
if !isHTTP {
|
||||
r.Cache.Add(destAddr, struct{}{})
|
||||
log.LogInfoWithAddr(srcAddr, destAddr, "sniff first request is not http, added to cache, switching to raw proxy")
|
||||
if sniff.SniffTLSClientHello(reader) {
|
||||
statistics.AddConnection(&statistics.ConnectionRecord{
|
||||
Protocol: sniff.TLS,
|
||||
SrcAddr: srcAddr,
|
||||
DestAddr: destAddr,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
statistics.AddConnection(&statistics.ConnectionRecord{
|
||||
Protocol: sniff.HTTP,
|
||||
SrcAddr: srcAddr,
|
||||
DestAddr: destAddr,
|
||||
})
|
||||
|
||||
var req *http.Request
|
||||
|
||||
for {
|
||||
if isHTTP, err = sniff.SniffHTTPFast(reader); err != nil {
|
||||
err = fmt.Errorf("sniff.SniffHTTPFast: %w", err)
|
||||
statistics.AddConnection(
|
||||
&statistics.ConnectionRecord{
|
||||
Protocol: sniff.TCP,
|
||||
SrcAddr: srcAddr,
|
||||
DestAddr: destAddr,
|
||||
},
|
||||
)
|
||||
return
|
||||
}
|
||||
if !isHTTP {
|
||||
@ -199,6 +224,11 @@ func (r *Rewriter) Process(dst net.Conn, src net.Conn, destAddr string, srcAddr
|
||||
}
|
||||
if req.Header.Get("Upgrade") == "websocket" && req.Header.Get("Connection") == "Upgrade" {
|
||||
log.LogInfoWithAddr(srcAddr, destAddr, "websocket upgrade detected, switching to raw proxy")
|
||||
statistics.AddConnection(&statistics.ConnectionRecord{
|
||||
Protocol: sniff.WebSocket,
|
||||
SrcAddr: srcAddr,
|
||||
DestAddr: destAddr,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,10 +4,13 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/sunbk201/ua3f/internal/log"
|
||||
"github.com/sunbk201/ua3f/internal/rewrite"
|
||||
"github.com/sunbk201/ua3f/internal/sniff"
|
||||
"github.com/sunbk201/ua3f/internal/statistics"
|
||||
)
|
||||
|
||||
var one = make([]byte, 1)
|
||||
@ -43,6 +46,8 @@ func CopyHalf(dst, src net.Conn) {
|
||||
|
||||
// ProxyHalf runs the rewriter proxy on src->dst and then half-closes both sides.
|
||||
func ProxyHalf(dst, src net.Conn, rw *rewrite.Rewriter, destAddr string) {
|
||||
srcAddr := src.RemoteAddr().String()
|
||||
|
||||
defer func() {
|
||||
if tc, ok := dst.(*net.TCPConn); ok {
|
||||
_ = tc.CloseWrite()
|
||||
@ -55,11 +60,18 @@ func ProxyHalf(dst, src net.Conn, rw *rewrite.Rewriter, destAddr string) {
|
||||
_ = src.Close()
|
||||
}
|
||||
log.LogDebugWithAddr(src.RemoteAddr().String(), destAddr, "Connections half-closed")
|
||||
defer statistics.RemoveConnection(srcAddr, destAddr)
|
||||
}()
|
||||
|
||||
// Fast path: known pass-through
|
||||
srcAddr := src.RemoteAddr().String()
|
||||
statistics.AddConnection(&statistics.ConnectionRecord{
|
||||
Protocol: sniff.TCP,
|
||||
SrcAddr: srcAddr,
|
||||
DestAddr: destAddr,
|
||||
StartTime: time.Now(),
|
||||
})
|
||||
|
||||
if rw.Cache.Contains(destAddr) {
|
||||
// Fast path: known pass-through
|
||||
log.LogInfoWithAddr(srcAddr, destAddr, fmt.Sprintf("destination (%s) in cache, passing through", destAddr))
|
||||
io.CopyBuffer(dst, src, one)
|
||||
return
|
||||
|
||||
@ -7,6 +7,17 @@ import (
|
||||
"io"
|
||||
)
|
||||
|
||||
// Protocol sniffed protocol types
|
||||
type Protocol string
|
||||
|
||||
const (
|
||||
TCP Protocol = "TCP"
|
||||
HTTP Protocol = "HTTP"
|
||||
HTTPS Protocol = "HTTPS"
|
||||
TLS Protocol = "TLS"
|
||||
WebSocket Protocol = "WebSocket"
|
||||
)
|
||||
|
||||
var ErrPeekTimeout = errors.New("peek timeout")
|
||||
|
||||
// peekLineSlice reads a line from bufio.Reader without consuming it.
|
||||
|
||||
77
src/internal/statistics/conn.go
Normal file
77
src/internal/statistics/conn.go
Normal file
@ -0,0 +1,77 @@
|
||||
package statistics
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/sunbk201/ua3f/internal/sniff"
|
||||
)
|
||||
|
||||
const connStatsFile = "/var/log/ua3f/conn_stats"
|
||||
|
||||
type ConnectionRecord struct {
|
||||
Protocol sniff.Protocol
|
||||
SrcAddr string
|
||||
DestAddr string
|
||||
StartTime time.Time
|
||||
}
|
||||
|
||||
type ConnectionAction struct {
|
||||
Action Action
|
||||
Key string
|
||||
Record ConnectionRecord
|
||||
}
|
||||
|
||||
var connectionRecords = make(map[string]*ConnectionRecord)
|
||||
|
||||
// AddConnection adds or updates a connection record
|
||||
func AddConnection(record *ConnectionRecord) {
|
||||
select {
|
||||
case connectionActionChan <- ConnectionAction{
|
||||
Action: Add,
|
||||
Key: fmt.Sprintf("%s-%s", record.SrcAddr, record.DestAddr),
|
||||
Record: *record,
|
||||
}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveConnection removes a connection record
|
||||
func RemoveConnection(srcAddr, destAddr string) {
|
||||
select {
|
||||
case connectionActionChan <- ConnectionAction{
|
||||
Action: Remove,
|
||||
Key: fmt.Sprintf("%s-%s", srcAddr, destAddr),
|
||||
}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func dumpConnectionRecords() {
|
||||
f, err := os.Create(connStatsFile)
|
||||
if err != nil {
|
||||
logrus.Errorf("create conn stats file error: %v", err)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var statList []ConnectionRecord
|
||||
for _, record := range connectionRecords {
|
||||
statList = append(statList, *record)
|
||||
}
|
||||
|
||||
// Sort by start time (newest first)
|
||||
sort.SliceStable(statList, func(i, j int) bool {
|
||||
return statList[i].StartTime.After(statList[j].StartTime)
|
||||
})
|
||||
|
||||
for _, record := range statList {
|
||||
duration := time.Since(record.StartTime)
|
||||
line := fmt.Sprintf("%s %s %s %s\n",
|
||||
record.Protocol, record.SrcAddr, record.DestAddr, duration.Round(time.Second))
|
||||
f.WriteString(line)
|
||||
}
|
||||
}
|
||||
@ -8,6 +8,15 @@ import (
|
||||
var (
|
||||
rewriteRecordChan = make(chan RewriteRecord, 2000)
|
||||
passThroughRecordChan = make(chan PassThroughRecord, 2000)
|
||||
connectionActionChan = make(chan ConnectionAction, 2000)
|
||||
)
|
||||
|
||||
// Actions for recording connection statistics
|
||||
type Action int
|
||||
|
||||
const (
|
||||
Add Action = iota
|
||||
Remove
|
||||
)
|
||||
|
||||
func StartRecorder() {
|
||||
@ -45,9 +54,26 @@ func StartRecorder() {
|
||||
Count: 1,
|
||||
}
|
||||
}
|
||||
case action := <-connectionActionChan:
|
||||
switch action.Action {
|
||||
case Add:
|
||||
if r, exists := connectionRecords[action.Key]; exists {
|
||||
r.Protocol = action.Record.Protocol
|
||||
} else {
|
||||
connectionRecords[action.Key] = &ConnectionRecord{
|
||||
Protocol: action.Record.Protocol,
|
||||
SrcAddr: action.Record.SrcAddr,
|
||||
DestAddr: action.Record.DestAddr,
|
||||
StartTime: action.Record.StartTime,
|
||||
}
|
||||
}
|
||||
case Remove:
|
||||
delete(connectionRecords, action.Key)
|
||||
}
|
||||
case <-ticker.C:
|
||||
dumpRewriteRecords()
|
||||
dumpPassThroughRecords()
|
||||
dumpConnectionRecords()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user