mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-15 04:53:00 +08:00
300 lines
9.3 KiB
Go
300 lines
9.3 KiB
Go
package session
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"net"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"UAmask/internal/config"
|
|
"UAmask/internal/events"
|
|
"UAmask/internal/model"
|
|
"UAmask/internal/policy"
|
|
"UAmask/internal/rewrite"
|
|
"UAmask/internal/state"
|
|
"UAmask/internal/stats"
|
|
)
|
|
|
|
type bufferConn struct {
|
|
reader *bytes.Buffer
|
|
writer *bytes.Buffer
|
|
}
|
|
|
|
func newReadConn(data string) *bufferConn {
|
|
return &bufferConn{reader: bytes.NewBufferString(data), writer: &bytes.Buffer{}}
|
|
}
|
|
|
|
func newWriteConn() *bufferConn {
|
|
return &bufferConn{reader: &bytes.Buffer{}, writer: &bytes.Buffer{}}
|
|
}
|
|
|
|
func (conn *bufferConn) Read(value []byte) (int, error) { return conn.reader.Read(value) }
|
|
func (conn *bufferConn) Write(value []byte) (int, error) { return conn.writer.Write(value) }
|
|
func (conn *bufferConn) Close() error { return nil }
|
|
func (conn *bufferConn) LocalAddr() net.Addr { return dummyAddr("local") }
|
|
func (conn *bufferConn) RemoteAddr() net.Addr { return dummyAddr("remote") }
|
|
func (conn *bufferConn) SetDeadline(time.Time) error { return nil }
|
|
func (conn *bufferConn) SetReadDeadline(time.Time) error { return nil }
|
|
func (conn *bufferConn) SetWriteDeadline(time.Time) error { return nil }
|
|
|
|
type dummyAddr string
|
|
|
|
func (address dummyAddr) Network() string { return "buffer" }
|
|
func (address dummyAddr) String() string { return string(address) }
|
|
|
|
type eventCollector struct {
|
|
events []model.SessionEvent
|
|
}
|
|
|
|
func (collector *eventCollector) Append(event model.SessionEvent) {
|
|
collector.events = append(collector.events, event)
|
|
}
|
|
|
|
func (collector *eventCollector) Count(eventType model.SessionEventType) int {
|
|
count := 0
|
|
for _, event := range collector.events {
|
|
if event.Type == eventType {
|
|
count++
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
func TestManagerProcessesActiveTrafficPath(t *testing.T) {
|
|
t.Run("PATH-HTTP-201 rewrites requests and projects facts", func(t *testing.T) {
|
|
manager, profiles, metrics, collector := newTrafficHarness(t, config.RewriteConfig{
|
|
UserAgent: "MaskUA",
|
|
ForceReplace: true,
|
|
}, config.FirewallConfig{})
|
|
sessionState := testSessionState(80)
|
|
src := newReadConn("GET / HTTP/1.1\r\nHost: example.com\r\nUser-Agent: A\r\n\r\n" +
|
|
"GET /x HTTP/1.1\r\nHost: example.com\r\nUser-Agent: B\r\n\r\n")
|
|
dst := newWriteConn()
|
|
|
|
manager.processClientToUpstream(&sessionState, src, dst)
|
|
|
|
if got := strings.Count(dst.writer.String(), "User-Agent: MaskUA"); got != 2 {
|
|
t.Fatalf("rewritten User-Agent count = %d, want 2; output: %s", got, dst.writer.String())
|
|
}
|
|
if snapshot := metrics.Snapshot(); snapshot.HTTPRequests != 2 || snapshot.ModifiedRequests != 2 {
|
|
t.Fatalf("stats snapshot = %+v, want 2 requests and modifications", snapshot)
|
|
}
|
|
if got := collector.Count(model.EventRewriteApplied); got != 2 {
|
|
t.Fatalf("rewrite facts = %d, want 2", got)
|
|
}
|
|
if snapshot := profiles.Snapshot(sessionState.Target); snapshot.LastEventType != model.EventRewriteApplied {
|
|
t.Fatalf("profile snapshot = %+v, want rewrite applied", snapshot)
|
|
}
|
|
})
|
|
|
|
t.Run("PATH-NONHTTP-201 falls back and updates target profile", func(t *testing.T) {
|
|
manager, profiles, metrics, collector := newTrafficHarness(t, config.RewriteConfig{
|
|
UserAgent: "MaskUA",
|
|
ForceReplace: true,
|
|
}, config.FirewallConfig{EnableBypass: true, NonHTTPThreshold: 2})
|
|
sessionState := testSessionState(22)
|
|
src := newReadConn("SSH-2.0-dropbear\r\n")
|
|
dst := newWriteConn()
|
|
|
|
manager.processClientToUpstream(&sessionState, src, dst)
|
|
|
|
if got := dst.writer.String(); got != "SSH-2.0-dropbear\r\n" {
|
|
t.Fatalf("fallback payload = %q", got)
|
|
}
|
|
if got := profiles.Snapshot(sessionState.Target).NonHTTPScore; got != 1 {
|
|
t.Fatalf("non-HTTP score = %d, want 1", got)
|
|
}
|
|
if got := collector.Count(model.EventNonHTTPObserved); got != 1 {
|
|
t.Fatalf("non-HTTP facts = %d, want 1", got)
|
|
}
|
|
if snapshot := metrics.Snapshot(); snapshot.HTTPRequests != 0 {
|
|
t.Fatalf("HTTP requests = %d, want 0", snapshot.HTTPRequests)
|
|
}
|
|
})
|
|
|
|
t.Run("PATH-DROP-201 firewall whitelist stops forwarding", func(t *testing.T) {
|
|
manager, profiles, _, collector := newTrafficHarness(t, config.RewriteConfig{
|
|
UserAgent: "MaskUA",
|
|
}, config.FirewallConfig{
|
|
UAWhitelist: []string{"BypassMe"},
|
|
DropOnMatch: true,
|
|
})
|
|
sessionState := testSessionState(443)
|
|
src := newReadConn("GET / HTTP/1.1\r\nHost: example.com\r\nUser-Agent: BypassMe/1.0\r\n\r\n")
|
|
dst := newWriteConn()
|
|
|
|
manager.processClientToUpstream(&sessionState, src, dst)
|
|
|
|
if got := dst.writer.String(); got != "" {
|
|
t.Fatalf("dropped request was forwarded: %q", got)
|
|
}
|
|
if got := collector.Count(model.EventRewriteSkipped); got != 1 {
|
|
t.Fatalf("rewrite skipped facts = %d, want 1", got)
|
|
}
|
|
snapshot := profiles.Snapshot(sessionState.Target)
|
|
if !snapshot.LastFirewallHit || snapshot.FirewallWhitelistHits != 1 {
|
|
t.Fatalf("firewall profile = %+v, want recorded whitelist hit", snapshot)
|
|
}
|
|
})
|
|
|
|
t.Run("PATH-FALLBACK-201 short protocol hint is preserved", func(t *testing.T) {
|
|
manager, profiles, _, collector := newTrafficHarness(t, config.RewriteConfig{}, config.FirewallConfig{})
|
|
sessionState := testSessionState(9000)
|
|
src := newReadConn("abc")
|
|
dst := newWriteConn()
|
|
|
|
manager.processClientToUpstream(&sessionState, src, dst)
|
|
|
|
if got := dst.writer.String(); got != "abc" {
|
|
t.Fatalf("fallback payload = %q, want abc", got)
|
|
}
|
|
if got := collector.Count(model.EventSessionForwardFallback); got != 1 {
|
|
t.Fatalf("fallback facts = %d, want 1", got)
|
|
}
|
|
if snapshot := profiles.Snapshot(sessionState.Target); snapshot.Found {
|
|
t.Fatalf("fallback-only event unexpectedly created profile: %+v", snapshot)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestManagerHandleContextClosesSilentUpstream(t *testing.T) {
|
|
clientPeer, acceptedConn := newTCPPair(t)
|
|
defer clientPeer.Close()
|
|
defer acceptedConn.Close()
|
|
|
|
upstreamListener, err := net.ListenTCP("tcp", &net.TCPAddr{IP: net.ParseIP("127.0.0.1")})
|
|
if err != nil {
|
|
t.Fatalf("LIFE-SESSION-202 listen upstream: %v", err)
|
|
}
|
|
defer upstreamListener.Close()
|
|
upstreamAccepted := make(chan *net.TCPConn, 1)
|
|
upstreamAcceptErr := make(chan error, 1)
|
|
go func() {
|
|
conn, acceptErr := upstreamListener.AcceptTCP()
|
|
if acceptErr != nil {
|
|
upstreamAcceptErr <- acceptErr
|
|
return
|
|
}
|
|
upstreamAccepted <- conn
|
|
}()
|
|
|
|
manager, _, _, collector := newTrafficHarness(t, config.RewriteConfig{}, config.FirewallConfig{})
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
done := make(chan struct{})
|
|
go func() {
|
|
manager.HandleContext(ctx, &model.AcceptedSession{
|
|
ClientConn: acceptedConn,
|
|
Target: model.Target{
|
|
Address: upstreamListener.Addr().String(),
|
|
IP: "127.0.0.1",
|
|
Port: upstreamListener.Addr().(*net.TCPAddr).Port,
|
|
},
|
|
IngressKind: model.IngressKindRedirect,
|
|
})
|
|
close(done)
|
|
}()
|
|
|
|
var upstreamPeer *net.TCPConn
|
|
select {
|
|
case upstreamPeer = <-upstreamAccepted:
|
|
case acceptErr := <-upstreamAcceptErr:
|
|
t.Fatalf("LIFE-SESSION-202 accept upstream: %v", acceptErr)
|
|
case <-time.After(time.Second):
|
|
t.Fatal("LIFE-SESSION-202 upstream connection was not established")
|
|
}
|
|
defer upstreamPeer.Close()
|
|
|
|
cancel()
|
|
select {
|
|
case <-done:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("LIFE-SESSION-202 manager did not stop after context cancellation")
|
|
}
|
|
if got := collector.Count(model.EventSessionClosed); got != 1 {
|
|
t.Fatalf("LIFE-SESSION-202 closed facts = %d, want 1", got)
|
|
}
|
|
}
|
|
|
|
func newTrafficHarness(
|
|
t *testing.T,
|
|
rewriteConfig config.RewriteConfig,
|
|
firewallConfig config.FirewallConfig,
|
|
) (*DefaultManager, *state.Store, *stats.Stats, *eventCollector) {
|
|
t.Helper()
|
|
cache, err := rewrite.NewUACache(16)
|
|
if err != nil {
|
|
t.Fatalf("new UA cache: %v", err)
|
|
}
|
|
profiles := state.NewStore(state.Config{
|
|
NonHTTPEnabled: firewallConfig.EnableBypass,
|
|
NonHTTPThreshold: firewallConfig.NonHTTPThreshold,
|
|
HTTPCooldownPeriod: time.Minute,
|
|
DecisionDelay: time.Second,
|
|
ProfileCleanupInterval: time.Hour,
|
|
})
|
|
metrics := stats.New()
|
|
collector := &eventCollector{}
|
|
dispatcher := events.NewDispatcher(profiles, metrics, collector)
|
|
manager := NewManager(
|
|
config.ListenConfig{BufferSize: 4096},
|
|
policy.NewSessionPolicy(),
|
|
rewrite.NewRequestRewriter(rewrite.NewEngine(rewriteConfig, config.RewriteRuntimeConfig{}, firewallConfig), cache),
|
|
profiles,
|
|
nil,
|
|
dispatcher,
|
|
)
|
|
return manager, profiles, metrics, collector
|
|
}
|
|
|
|
func testSessionState(port int) model.SessionState {
|
|
return model.SessionState{
|
|
ID: 1,
|
|
IngressKind: model.IngressKindRedirect,
|
|
Target: model.Target{
|
|
Address: "203.0.113.30:" + strconv.Itoa(port),
|
|
IP: "203.0.113.30",
|
|
Port: port,
|
|
},
|
|
}
|
|
}
|
|
|
|
func newTCPPair(t *testing.T) (*net.TCPConn, *net.TCPConn) {
|
|
t.Helper()
|
|
listener, err := net.ListenTCP("tcp", &net.TCPAddr{IP: net.ParseIP("127.0.0.1")})
|
|
if err != nil {
|
|
t.Fatalf("listen TCP pair: %v", err)
|
|
}
|
|
accepted := make(chan *net.TCPConn, 1)
|
|
acceptErr := make(chan error, 1)
|
|
go func() {
|
|
conn, err := listener.AcceptTCP()
|
|
if err != nil {
|
|
acceptErr <- err
|
|
return
|
|
}
|
|
accepted <- conn
|
|
}()
|
|
peer, err := net.DialTCP("tcp", nil, listener.Addr().(*net.TCPAddr))
|
|
if err != nil {
|
|
_ = listener.Close()
|
|
t.Fatalf("dial TCP pair: %v", err)
|
|
}
|
|
var server *net.TCPConn
|
|
select {
|
|
case server = <-accepted:
|
|
case err := <-acceptErr:
|
|
_ = peer.Close()
|
|
_ = listener.Close()
|
|
t.Fatalf("accept TCP pair: %v", err)
|
|
case <-time.After(time.Second):
|
|
_ = peer.Close()
|
|
_ = listener.Close()
|
|
t.Fatal("timed out accepting TCP pair")
|
|
}
|
|
_ = listener.Close()
|
|
return peer, server
|
|
}
|