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
3
.gitignore
vendored
3
.gitignore
vendored
@ -24,4 +24,5 @@ go.work
|
|||||||
bin/
|
bin/
|
||||||
dist/
|
dist/
|
||||||
ipkg/etc/
|
ipkg/etc/
|
||||||
ipkg/usr/
|
ipkg/usr/
|
||||||
|
test/
|
||||||
@ -23,6 +23,18 @@ if pass_stats_file then
|
|||||||
pass_stats_file:close()
|
pass_stats_file:close()
|
||||||
end
|
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)
|
local function rowstyle(i)
|
||||||
return (i % 2 == 0) and "cbi-rowstyle-2" or "cbi-rowstyle-1"
|
return (i % 2 == 0) and "cbi-rowstyle-2" or "cbi-rowstyle-1"
|
||||||
end
|
end
|
||||||
@ -30,13 +42,13 @@ end
|
|||||||
|
|
||||||
<h3><%:Statistics%></h3>
|
<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">
|
<table id="rewrite-stats-table" class="table cbi-section-table">
|
||||||
<tr class="tr table-titles">
|
<tr class="tr table-titles">
|
||||||
<th class="th"><%:Host%></th>
|
<th class="th" data-sortable-row="true"><%:Host%></th>
|
||||||
<th class="th"><%:Rewrite Count%></th>
|
<th class="th" data-sortable-row="true"><%:Rewrite Count%></th>
|
||||||
<th class="th"><%:Original User-Agent%></th>
|
<th class="th" data-sortable-row="true"><%:Original User-Agent%></th>
|
||||||
<th class="th"><%:Modified User-Agent%></th>
|
<th class="th" data-sortable-row="true"><%:Modified User-Agent%></th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<% for i, item in ipairs(rewrite_stats) do %>
|
<% for i, item in ipairs(rewrite_stats) do %>
|
||||||
@ -49,13 +61,13 @@ end
|
|||||||
<% end %>
|
<% end %>
|
||||||
</table>
|
</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">
|
<table id="pass-stats-table" class="table cbi-section-table">
|
||||||
<tr class="tr table-titles">
|
<tr class="tr table-titles">
|
||||||
<th class="th"><%:User-Agent%></th>
|
<th class="th" data-sortable-row="true"><%:User-Agent%></th>
|
||||||
<th class="th"><%:Pass-Through Count%></th>
|
<th class="th" data-sortable-row="true"><%:Pass-Through Count%></th>
|
||||||
<th class="th"><%:Last Source Address%></th>
|
<th class="th" data-sortable-row="true"><%:Last Source Address%></th>
|
||||||
<th class="th"><%:Last Destination Address%></th>
|
<th class="th" data-sortable-row="true"><%:Last Destination Address%></th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<% for i, item in ipairs(pass_stats) do %>
|
<% for i, item in ipairs(pass_stats) do %>
|
||||||
@ -68,6 +80,30 @@ end
|
|||||||
<% end %>
|
<% end %>
|
||||||
</table>
|
</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">
|
<script type="text/javascript">
|
||||||
async function updateStats() {
|
async function updateStats() {
|
||||||
@ -79,6 +115,7 @@ end
|
|||||||
const doc = parser.parseFromString(text, "text/html");
|
const doc = parser.parseFromString(text, "text/html");
|
||||||
const newRewriteTable = doc.querySelector("#rewrite-stats-table");
|
const newRewriteTable = doc.querySelector("#rewrite-stats-table");
|
||||||
const newPassTable = doc.querySelector("#pass-stats-table");
|
const newPassTable = doc.querySelector("#pass-stats-table");
|
||||||
|
const newConnTable = doc.querySelector("#conn-stats-table");
|
||||||
const newLog = doc.querySelector("#cbid\\.ua3f\\.main\\.log");
|
const newLog = doc.querySelector("#cbid\\.ua3f\\.main\\.log");
|
||||||
|
|
||||||
if (newRewriteTable) {
|
if (newRewriteTable) {
|
||||||
@ -87,6 +124,9 @@ end
|
|||||||
if (newPassTable) {
|
if (newPassTable) {
|
||||||
document.querySelector("#pass-stats-table").innerHTML = newPassTable.innerHTML;
|
document.querySelector("#pass-stats-table").innerHTML = newPassTable.innerHTML;
|
||||||
}
|
}
|
||||||
|
if (newConnTable) {
|
||||||
|
document.querySelector("#conn-stats-table").innerHTML = newConnTable.innerHTML;
|
||||||
|
}
|
||||||
if (newLog) {
|
if (newLog) {
|
||||||
document.querySelector("#cbid\\.ua3f\\.main\\.log").value = newLog.value;
|
document.querySelector("#cbid\\.ua3f\\.main\\.log").value = newLog.value;
|
||||||
}
|
}
|
||||||
@ -140,4 +180,25 @@ end
|
|||||||
#pass-stats-table td:nth-child(4) {
|
#pass-stats-table td:nth-child(4) {
|
||||||
width: 30%;
|
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>
|
</style>
|
||||||
|
|||||||
@ -108,4 +108,22 @@ msgid "Set TTL"
|
|||||||
msgstr "固定 TTL"
|
msgstr "固定 TTL"
|
||||||
|
|
||||||
msgid "Set the TTL 64 for packets"
|
msgid "Set the TTL 64 for packets"
|
||||||
msgstr "固定数据包的 TTL"
|
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) {
|
if strings.HasSuffix(destAddr, "443") && sniff.SniffTLSClientHello(reader) {
|
||||||
r.Cache.Add(destAddr, struct{}{})
|
r.Cache.Add(destAddr, struct{}{})
|
||||||
log.LogInfoWithAddr(srcAddr, destAddr, "tls client hello detected, added to cache")
|
log.LogInfoWithAddr(srcAddr, destAddr, "tls client hello detected, added to cache")
|
||||||
|
statistics.AddConnection(&statistics.ConnectionRecord{
|
||||||
|
Protocol: sniff.HTTPS,
|
||||||
|
SrcAddr: srcAddr,
|
||||||
|
DestAddr: destAddr,
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -172,14 +177,34 @@ func (r *Rewriter) Process(dst net.Conn, src net.Conn, destAddr string, srcAddr
|
|||||||
if !isHTTP {
|
if !isHTTP {
|
||||||
r.Cache.Add(destAddr, struct{}{})
|
r.Cache.Add(destAddr, struct{}{})
|
||||||
log.LogInfoWithAddr(srcAddr, destAddr, "sniff first request is not http, added to cache, switching to raw proxy")
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
statistics.AddConnection(&statistics.ConnectionRecord{
|
||||||
|
Protocol: sniff.HTTP,
|
||||||
|
SrcAddr: srcAddr,
|
||||||
|
DestAddr: destAddr,
|
||||||
|
})
|
||||||
|
|
||||||
var req *http.Request
|
var req *http.Request
|
||||||
|
|
||||||
for {
|
for {
|
||||||
if isHTTP, err = sniff.SniffHTTPFast(reader); err != nil {
|
if isHTTP, err = sniff.SniffHTTPFast(reader); err != nil {
|
||||||
err = fmt.Errorf("sniff.SniffHTTPFast: %w", err)
|
err = fmt.Errorf("sniff.SniffHTTPFast: %w", err)
|
||||||
|
statistics.AddConnection(
|
||||||
|
&statistics.ConnectionRecord{
|
||||||
|
Protocol: sniff.TCP,
|
||||||
|
SrcAddr: srcAddr,
|
||||||
|
DestAddr: destAddr,
|
||||||
|
},
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !isHTTP {
|
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" {
|
if req.Header.Get("Upgrade") == "websocket" && req.Header.Get("Connection") == "Upgrade" {
|
||||||
log.LogInfoWithAddr(srcAddr, destAddr, "websocket upgrade detected, switching to raw proxy")
|
log.LogInfoWithAddr(srcAddr, destAddr, "websocket upgrade detected, switching to raw proxy")
|
||||||
|
statistics.AddConnection(&statistics.ConnectionRecord{
|
||||||
|
Protocol: sniff.WebSocket,
|
||||||
|
SrcAddr: srcAddr,
|
||||||
|
DestAddr: destAddr,
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,10 +4,13 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/sunbk201/ua3f/internal/log"
|
"github.com/sunbk201/ua3f/internal/log"
|
||||||
"github.com/sunbk201/ua3f/internal/rewrite"
|
"github.com/sunbk201/ua3f/internal/rewrite"
|
||||||
|
"github.com/sunbk201/ua3f/internal/sniff"
|
||||||
|
"github.com/sunbk201/ua3f/internal/statistics"
|
||||||
)
|
)
|
||||||
|
|
||||||
var one = make([]byte, 1)
|
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.
|
// 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) {
|
func ProxyHalf(dst, src net.Conn, rw *rewrite.Rewriter, destAddr string) {
|
||||||
|
srcAddr := src.RemoteAddr().String()
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if tc, ok := dst.(*net.TCPConn); ok {
|
if tc, ok := dst.(*net.TCPConn); ok {
|
||||||
_ = tc.CloseWrite()
|
_ = tc.CloseWrite()
|
||||||
@ -55,11 +60,18 @@ func ProxyHalf(dst, src net.Conn, rw *rewrite.Rewriter, destAddr string) {
|
|||||||
_ = src.Close()
|
_ = src.Close()
|
||||||
}
|
}
|
||||||
log.LogDebugWithAddr(src.RemoteAddr().String(), destAddr, "Connections half-closed")
|
log.LogDebugWithAddr(src.RemoteAddr().String(), destAddr, "Connections half-closed")
|
||||||
|
defer statistics.RemoveConnection(srcAddr, destAddr)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Fast path: known pass-through
|
statistics.AddConnection(&statistics.ConnectionRecord{
|
||||||
srcAddr := src.RemoteAddr().String()
|
Protocol: sniff.TCP,
|
||||||
|
SrcAddr: srcAddr,
|
||||||
|
DestAddr: destAddr,
|
||||||
|
StartTime: time.Now(),
|
||||||
|
})
|
||||||
|
|
||||||
if rw.Cache.Contains(destAddr) {
|
if rw.Cache.Contains(destAddr) {
|
||||||
|
// Fast path: known pass-through
|
||||||
log.LogInfoWithAddr(srcAddr, destAddr, fmt.Sprintf("destination (%s) in cache, passing through", destAddr))
|
log.LogInfoWithAddr(srcAddr, destAddr, fmt.Sprintf("destination (%s) in cache, passing through", destAddr))
|
||||||
io.CopyBuffer(dst, src, one)
|
io.CopyBuffer(dst, src, one)
|
||||||
return
|
return
|
||||||
|
|||||||
@ -7,6 +7,17 @@ import (
|
|||||||
"io"
|
"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")
|
var ErrPeekTimeout = errors.New("peek timeout")
|
||||||
|
|
||||||
// peekLineSlice reads a line from bufio.Reader without consuming it.
|
// 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 (
|
var (
|
||||||
rewriteRecordChan = make(chan RewriteRecord, 2000)
|
rewriteRecordChan = make(chan RewriteRecord, 2000)
|
||||||
passThroughRecordChan = make(chan PassThroughRecord, 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() {
|
func StartRecorder() {
|
||||||
@ -45,9 +54,26 @@ func StartRecorder() {
|
|||||||
Count: 1,
|
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:
|
case <-ticker.C:
|
||||||
dumpRewriteRecords()
|
dumpRewriteRecords()
|
||||||
dumpPassThroughRecords()
|
dumpPassThroughRecords()
|
||||||
|
dumpConnectionRecords()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user