Back to "StyloBot-utsläppsserien: Sidecars arkitektur"

This is a viewer only at the moment see the article on how this works.

To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk

This is a preview from the server running through my markdig pipeline

Architecture ASP.NET Core Go gRPC StyloBot

StyloBot-utsläppsserien: Sidecars arkitektur

Monday, 01 June 2026

StyloBots detektormotor är ASP, ., NET Core och .. Denna artikel förklarar hur den maskinen kopplas till Go-grindar, ,, noder och ., js-applikationer, ,, och alla andra stakar via en gRPC-bilar, mska6, en typad Go SDK, mske7, och ett Caddy-plugin, , utan att någon av dessa kunder behöver veta något om . och ..

StyloBot

Den github.com/scottgal/stylobot-go SDK, github.com/scottgal/caddy-stylobot plugin, och Mostlylucid.BotDetection.Sidecar behållare kommer att publiceras snart. Allt som följer beskriver ytan de ska utsätta

StyloBot-utsläppsserien

  1. Behavior, Inte Identitet: varför StyloBot modellerar klienter beteendemässigt
  2. Behavior-Är medveten ASP.NET UI: servern
  3. Att hitta och lösa gränslös tillväxt i långtids-, -- och / eller körande- -.-nätverk: den tillförlitlighetsdisciplin som gör motorn tråkig i produktionen
  4. Behavior-Akvis typscript-UI: Express , Fastify, och webbladerkomponenter
  5. Sidecar-arkitekturen: den här artikeln
  6. Att lära sig bli snabbare: adaptiv inlärningssystem, ,, fyra, M SK2, övningsminnet, ,, och bedömningsキャッシュen
  7. Att testa det som inte stannar kvar: kontrolldisciplinen : en BDF-fielddriver regression M SK2 belastning , och kalibrering
  8. StyloExtract - en lokal lärande HTML omvandlare till Markdown: HTML, →, Markdown-lagret som paras med detektorn, ,, walkersbug lucidVIEW som fångade, M SK3, och den hundföda-loopan som gjorde det ärligt

Varför en bilar

En omvänd proxy är den uppenbara platsen för att köra botdetektorn. göra annorlunda baserat på vem som gör begäran.

Sidekarets mönster skiljer de två problemna från varandra. Gatewayt stannar snabbt och ostämt . Sidekaret håller kvar sessie tillståndet, archetyp-ankernM SK3 drift tillståndet~, och slutsatsen-cache som gör detektorn exakta | ( archtypen-ankermodellenM SK7 fingeravtryckets slutsatss-caché~MSC9 och de fyra levels-lärningssystemen är täckt av Att lära sig bli snabbare). De två kommunicerar över det lokala nätverket | ( | samma bedienare eller samma Pod | ) | så den runda färden |- | är mikrosekunder till en enda |

Detta är inget nytt mönster. Envoy Proxy gör precis detta för tjänsten-meshbekymmer | ( | mTLS |, | återfördrag | Dapr gör det för staten och puben OpenTelemetry Kollektor gör det för telemetrin . Linkerd, Consul Connect, och AWS App Mesh alla följer samma modell . Patternen fortsätter att dyka upp eftersom den löser ett riktigt problem M SK4 du vill ha ett komplext tillståndsmässigt beteende som passerar språkgränserna utan att implementera det i varje språk

Varför inte bara lägga in den som en bibliotek?

Alternativet är att kompilera detektorn direkt i portaalet.

Inget av dem är realistiskt för en motor med den här komplexiteten. vågor Later waves fire only when earlier signals warrant it , ;, a credential, -, theuffing attempt triggers different detectors than a Googlebot crawl Markovkedjans vektorer i ett dimensionellt utrymme... En Markov-skedja är bara en möjlighetsmodell över ". Med tanke på det sista som den här sammankomsten gjorde, ,. Vad kommer nästa gång? ?", och 129. Dimensionerna fångar tillräckligt med sidor. Leidens samhällsdetektion över dessa vektorer, :, en graf, -, en skrämmande algoritm som gruppar samman sestioner som beter sig på samma sätt.,, vilket är hur StyloBot upptäcker ett botnätverk även när enskilda sestioner ser bra ut.., och det fortsätter med allt detta till SQLite mellan begäran.M SK4, den här tillståndet behöver en oberoende livscykel. ., den kan inte börja om med noderprocessen eller bli sönderslagen när portaal laddar upp sin konfiguration igen.

Ett bilar låter varje komponent göra vad det är bra på

graph TD
    classDef input fill:none,stroke:#3b82f6,stroke-width:2px
    classDef async fill:none,stroke:#a855f7,stroke-width:2px
    classDef good fill:none,stroke:#22c55e,stroke-width:2px
    classDef store fill:none,stroke:#f59e0b,stroke-width:2px

    GW["Gateway<br/>Caddy / YARP / nginx"]:::input
    SD["StyloBot Sidecar · ASP.NET Core<br/>gRPC :5090 · REST :5091<br/>≤50ms per Detect RPC"]:::async
    APP["Upstream Application<br/>Node / Go / ASP.NET<br/>reads req.stylobot.verdict"]:::good
    DB[("SQLite<br/>sessions · signatures · reputation")]:::store

    GW --> SD
    SD <--> DB
    GW --> APP

Gatewayn ropar på sidekaren, ,, injicerar resultatet som HTTP-header och blockerar det optionellt.. Uppströmsapplikationen läser ut headerna och sätter beslutet.

Avgasbilen

Mostlylucid.BotDetection.Sidecar är en minimal ASP-process.

  • :5090: HTTP
  • :5091: HTTP /1.1 för REST-clienter /api/v1/* slutpunkter

gRPC

gRPC är en högpresterande fjärranvedningssystem applikation som utvecklats på Google. Protokollbuffer (protobuf

Interfejsen definieras i .proto file. Från den filen , kogenerer producerar inskrivna klient- och server-stubs i vilket språk som helst .proto så att alla språk med en gRPC implementering kan kalla sidecar

GRP: s gränssnitt

Servicen har tre RPCs:

service DetectionService {
  rpc Detect(DetectRequest)             returns (DetectResponse);
  rpc DetectBatch(DetectBatchRequest)   returns (DetectBatchResponse);
  rpc RenderWidget(RenderWidgetRequest) returns (RenderWidgetResponse);
}

Detect is the per-request hot path| .| Pass it method| ,| path|,| header| MST4| remote IP| M ST5| and optional TLS fingerprint data| M st6| It runs the wave pipeline | M St7| only the detectors that the request| M S S K 8| its signals warrant| M s K 9| updates the session vector _ M S K 10| scores against the reputation store| M L S K 11| and returns a verdict| M R S K 12

DetectBatch kör flera applikationer i ordning. Används för logrepetition och avgränsad analys, inte perM SK2 användandet av kopplingen till applikationen

RenderWidget accepterar en flytande templatesträng, en optionell bedömelse , och ett nyckel-värdekartan av ytterligare variablerM SK3 sedan renderar templateservernMska4 sida och återvänder HTMLM Ska5 Det här är hur icke-Mska6 NET-kolar producerar botMske7 medveten HTML utan att sätta upp en separat renderprocessMka8 detaljer i RenderWidget-sektionen härnästMkka9

Vad händer inuti detektorn

En snabb Glossar före diagrammet, eftersom dessa namn kommer att dyka upp

  • Blackboard: en per-beställningsnyckl -värdesväska (e.gM SK5 request.ip.is_datacenter, detection.useragent.confidence). Detektorer skriver signaler till den ; Senare detektorer i samma våg läser dem . Den lever bara under en gång av ett begäran.
  • Syntetisk Http Kontext: en stomme HttpContext den gRPC-tjänsterna bygger från protoanfragens fälten. Detektormotorn var designad för ASP.NET mellanverk och förväntar sig att läsa från HttpContext; genom att syntetisera ett får samma motor att fungera oföränderligt inuti en gRPC sändning
  • Detektorinsatser / Aggregerade bevis: detektors utgångar (signal skriver ♫ + ♫ självförtroende deltas ♫) ♫ och deras sammanlagda resultat ♫
sequenceDiagram
    participant GW as Gateway (Caddy)
    participant SD as gRPC Service
    participant ORC as BlackboardOrchestrator
    participant DET as Detectors (up to 49, 4 waves)
    participant DB as SQLite

    GW->>SD: Detect RPC { method, path, headers, remoteIp }
    SD->>ORC: DetectAsync(syntheticHttpContext)
    ORC->>DET: Wave 0 - Identity + ContentSequence
    ORC->>DET: Wave 1 - Fast path <1ms: UA, Header, IP, Heuristic ...
    ORC->>DET: Wave 2 - Session vectors, Behavioural waveform
    ORC->>DET: Wave 3 - Slow path: DNS, advanced fingerprinting
    DET-->>ORC: DetectionContributions (signals, confidence deltas)
    ORC->>DB: update session vector and reputation score
    DB-->>ORC: ok
    ORC-->>SD: AggregatedEvidence { botProbability, riskBand, ... }
    SD-->>GW: DetectResponse { isBot, riskBand, recommendedAction, ... }

Hela pipelinen körs inuti en enda gRPC sändning.

Go SDK

Gateway-koden i Go kan inte importera ASP.NET sidecar. Det som den kan göra är att kalla den över gRPC . Go SDK M SK3github.com/scottgal/stylobot-go) ger en skriftippd gränssnitt som gömmer genererade protobuf-typer helt från budbärare

Varför gömmer SDK protobuf-typer

Protobuf-genererad kod är verbos och har en ovanlig API . Enamer är representerade som integerRISK_BAND_HIGH, inte "High"). Feldnamen är kameelKlas i vissa generatorer och slang _ fall i andra M SK2 Att utsätta prototyper i din offentliga API betyder att dina telefoner måste förstå allt detta

SDK översätter en gång vid gränsen (proto-enum till kanoniska strängar , protostrukturer till enkla Go-strukturer ) och telefonerna ser det aldrig

// the only interface you depend on: no proto imports required
type Client interface {
    Detect(ctx context.Context, req DetectRequest) (*Verdict, error)
    DetectBatch(ctx context.Context, reqs []DetectRequest) ([]*Verdict, error)
    RenderWidget(ctx context.Context, req RenderRequest) (*RenderResponse, error)
    Close() error
}

DetectRequest och Verdict är enkla Go-strukturer:

type DetectRequest struct {
    Method   string
    Path     string
    Headers  map[string]string
    RemoteIP string
    Protocol string  // "http" or "https"; defaults to "https" if empty
    TLS      *TLSInfo
}

type Verdict struct {
    IsBot             bool
    BotProbability    float32
    Confidence        float32
    BotType           string   // "AiBot", "Scraper", "GoodBot", ...
    BotName           string
    RiskBand          string   // "VeryLow", "Low", "Elevated", "Medium", "High", "VeryHigh"
    RecommendedAction string   // "Allow", "Throttle", "Challenge", "Block"
    ThreatScore       float32
    ThreatBand        string
    ProcessingTimeMs  float32
    DetectorsRun      int32
    Reasons           []Reason
}

Att skapa en klient och köra detektorn:

import (
    stylobot "github.com/scottgal/stylobot-go"
    "context"
    "time"
)

client, err := stylobot.NewClient(
    "localhost:5090",
    stylobot.WithTimeout(50 * time.Millisecond),
    stylobot.WithAPIKey(os.Getenv("SB_API_KEY")),
)
if err != nil {
    log.Fatal(err)
}
defer client.Close()

verdict, err := client.Detect(ctx, stylobot.DetectRequest{
    Method:   r.Method,
    Path:     r.URL.RequestURI(),
    RemoteIP: r.RemoteAddr,
    Headers:  extractHeaders(r),
    Protocol: "https",
})
if err != nil {
    // fail open: log and continue
    log.Printf("stylobot detect failed: %v", err)
    return next(w, r)
}

if verdict.RecommendedAction == "Block" {
    http.Error(w, "Forbidden", http.StatusForbidden)
    return
}

Lysslig koppling och uppstartningssäkerhet

grpc.NewClient skapar en klientkanal, men bygger inte direkt en TCP-uppkoppling.. Anslutningen sker vid den första RPC: s sändning.

Detta skiljer sig från HTTP-clienter, där du vanligtvis ansluter dig vid skapandet. gRPC gå med dokumentation täcker livscykeln i detalj.

Tyddump-interaktion

WithTimeoutNewClient sätter en standardinställning per call deadline som tillämpas inuti varje Detect ringa. Om din ringkod | ( | eller mellanverk som Caddy-plugin |) | redan drar fram ett deadline | МSK3 | begränsat sammanhang från den inkomende utmaningen ♫ , | så applicerar SDK vilken deadline som helst först stängs ut ♫ WithTimeout från NewClient och låter pluginen kontrollera det. För att använda självständigt | ( | en handledare som ropar direkt på SDK |), | sätta igång NewClient som visas ovanför.

Caddy- plugin

Caddy är en Go-based webbserver och omvänd proxy med automatisk HTTPS xcaddy att bygga ett anpassad Caddy-binär som innefattar dina plugins, som producerar en enda self-contained binary utan beroende av utloppstid på de delade biblioteken . Det här skiljer sig från nginxMSC3s dynamiska modulsystem M SK4.so fileloaded at runtime). StyloBots plugin (github.com/scottgal/caddy-stylobot) registrerar en middleware-handlerare som ropar på Go SDK varje gång

Konfigurationen av Caddyfilen:

{
    order stylobot before respond
}

:80 {
    stylobot {
        endpoint localhost:5090   # gRPC host:port of the sidecar
        timeout   50ms            # per-request deadline; fails open on expiry
        # on_block 503            # optional: change the block status code (default: 403)
    }
    reverse_proxy upstream:3000
}

Plug-in injicerar nio slutsatser på varje skickad pyyntö:

Titel källfält
X-StyloBot-IsBot isBot (bool
X-StyloBot-Probability botProbability (0.0-1.0)
X-StyloBot-Confidence confidence (0.0-1.0)
X-StyloBot-BotType e.gM SK2 AiBot, Scraper, GoodBot
X-StyloBot-BotName e.gM SK2 GPTBot, Googlebot
X-StyloBot-RiskBand VeryLow ... VeryHigh
X-StyloBot-Action Allow / Throttle / Challenge / Block
X-StyloBot-ThreatScore numerisk
X-StyloBot-ThreatBand None ... Critical

Tillåter var isBot=true och Action=Block stannar vid öppningsplatsen med en 403 och når aldrig uppströmmen | . | Allt annat |( | inklusive robotar med Throttle eller Challenge tillämpning ) skickas vidare med alla nio opskrifte intakt. Det är den avsedda splittningen : portaalet hanterar hårda blocker M SK3 uppströmmen hanterar nyanser

on_block ändrar statuskoden som används när portaal blockerar (defaultM SK1 403). Set on_block 503 för att hämma återtestningslogiken i skalare som behandlar 403 som återtestbart .

Vad mellanvaran gör vid varje begäran

flowchart TD
    classDef input fill:none,stroke:#3b82f6,stroke-width:2px
    classDef async fill:none,stroke:#a855f7,stroke-width:2px
    classDef good fill:none,stroke:#22c55e,stroke-width:2px

    A["1. Strip inbound X-StyloBot-* headers"]:::input
    B["2. context.WithTimeout(r.Context(), 50ms)"]:::input
    C["3. sbClient.Detect(ctx, DetectRequest)"]:::async
    D{error?}
    E["log warn - fail open<br/>forward unchanged"]:::good
    F["4. injectHeaders<br/>X-StyloBot-IsBot, Probability, Confidence,<br/>BotType, BotName, RiskBand, Action,<br/>ThreatScore, ThreatBand"]:::input
    I["next.ServeHTTP - forward to upstream<br/>with all verdict headers injected"]:::good

    A --> B --> C --> D
    D -->|yes| E --> I
    D -->|no| F --> I

steg 1, strålkastar inbound En klient som känner till X-StyloBot-* headernamen skulle kunna göra sig själva-injicera ett positivt dömande och få den att överleva misslyckandet -öppen väg . Att ta bort dem först betyder att dömet uppströms ser alltid kom från bilarna

steg 2, kontextuell termin Tyduppgången kommer från r.Context() med hjälp av context.WithTimeout ( som tar en relativt lång tid context.WithDeadline tar en absolut tid. r.Context() snarare än context.Background() är nyckelpunkten : om klienten avskärmar sig innan gRPC-uppdraget slutar, avbrytningen sprider sig genom och bibilen slutar processen tidigt

steg 3 och 4, att upptäcka och injicera Nio fällor blir nio X-StyloBot-* opskrifte. opskrifte är ställda innan blockchecken, så uppströmmen läser dem via styloBotMiddleware({ mode: 'headers' }) för alla icke-- blockerade begäran isBot=true och recommendedAction=Block kommer tillbaka som 403 vid öppningsplatsen ; allt annat går framåt med fulla bedömande naglarna anslutna

Implementeringen:

// from sdk/caddy/stylobot.go
func (s *StyloBot) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
    for _, name := range stylobotHeaders {
        r.Header.Del(name)
    }

    ctx, cancel := context.WithTimeout(r.Context(), s.timeout)
    defer cancel()

    verdict, err := s.sbClient.Detect(ctx, sb.DetectRequest{
        Method:   r.Method,
        Path:     r.URL.RequestURI(),
        RemoteIP: ExtractIP(r),
        Protocol: r.Proto,
        Headers:  ExtractHeaders(r),
    })
    if err != nil {
        s.logger.Warn("stylobot detect failed, failing open", zap.Error(err))
        return next.ServeHTTP(w, r)
    }

    injectHeaders(r, verdict)

    if verdict.IsBot && s.OnBlock > 0 && verdict.RecommendedAction == "Block" {
        http.Error(w, "Forbidden", s.OnBlock)
        return nil
    }
    return next.ServeHTTP(w, r)
}

Byggnader med xcaddy

Caddy-plugins måste kompileras i binären med hjälp av xcaddy. Dockerfilen i integreringstesten visar mönstret

# from tests/integration/caddy-sidecar/Dockerfile
FROM caddy:2-builder AS builder

WORKDIR /build
COPY sdk/caddy/ caddy-plugin/
COPY sdk/go/    go/

WORKDIR /build/caddy-plugin

RUN xcaddy build \
    --with github.com/scottgal/caddy-stylobot=/build/caddy-plugin \
    --with github.com/scottgal/stylobot-go=/build/go

FROM caddy:2
COPY --from=builder /build/caddy-plugin/caddy /usr/bin/caddy
COPY tests/integration/caddy-sidecar/Caddyfile /etc/caddy/Caddyfile

Direktiven som ersätter xcaddy

Pluginet's go.mod innehåller:

replace github.com/scottgal/stylobot-go => ../go

Detta säger till Go toolchain " när du ser stylobot-go, använda det lokala kataloget istället för att hämta från proxymodulen go build och go test i plugin-katalogen.

xcaddy skapar ett nytt temporärt Go-modul för sin byggnad. Det modulet är inte härvt replace instruktioner från pluginn's go.mod. Utan den andra --with argument, xcaddy skulle försöka ladda ner stylobot-go från pkg.go.dev (där det inte är ännu publicerat

Den --with module=path argumentet är xcaddy's ursprungliga motsvarighet till replace direktiv: den kartlägger en modulväg till ett lokalt gids vid byggnadstid

RenderWidget: Flytande templater över gRPC

RenderWidget är en gRPC RPC på sidecarn som accepterar en Flüssig templatesträng, renderar den med det detekterande sammanhanget , och återvänder HTML. Det låter vilken som helst som ska ringa M SK3 gå till proxy~,~ Nod SSR-layer , lotskedjan~)~ skapa botMSC7 som vet HTML utan att köra en separat renderprocessM SK8

Vorlager för flytande

Flytande är en templeringsspråk skapad av Shopify, som används av shopifys Themes, JekyllM SK2 GitHub Pages , och många andra systemMSC4 dess nyckeleigenheter : säkra att köra med användaren M SK6 tillhandahållna templater | ( inga godtyckliga kodöverdrag |), enkelt nog för icke- Fluid.Core, en hög -presterande

Implementering av bilar:

// from src/Mostlylucid.BotDetection.Sidecar/Services/DetectionGrpcService.cs
private static readonly FluidParser Parser = new();  // static, shared, compiled templates cached

public override async Task<Proto.RenderWidgetResponse> RenderWidget(
    Proto.RenderWidgetRequest request, ServerCallContext context)
{
    if (!Parser.TryParse(request.Template, out var template, out var error))
        return new Proto.RenderWidgetResponse { Success = false, Error = error };

    var ctx = new TemplateContext();
    if (request.Verdict is { } v)
    {
        ctx.SetValue("isBot",             v.IsBot);
        ctx.SetValue("botProbability",    (double)v.BotProbability);
        ctx.SetValue("botType",           v.BotType);
        ctx.SetValue("botName",           v.BotName);
        ctx.SetValue("riskBand",          v.RiskBand.ToString());
        ctx.SetValue("recommendedAction", v.RecommendedAction.ToString());
        ctx.SetValue("threatScore",       (double)v.ThreatScore);
        ctx.SetValue("threatBand",        v.ThreatBand.ToString());
    }
    foreach (var kv in request.Vars)
        ctx.SetValue(kv.Key, kv.Value);

    var html = await template.RenderAsync(ctx);
    return new Proto.RenderWidgetResponse { Html = html, Success = true };
}

Fluid.Core upprätthåller en inre kompilerad template-cache FluidParser är statisk och de delar ut sig över alla gRPC sändningar.

Noden StyloBotGrpcClient.renderWidget() exemplet och fulla templatevariablereferens finns i Artikel om TypeScript SDK.

Jag kallar det från Go:

rendered, err := client.RenderWidget(ctx, stylobot.RenderRequest{
    Template: `{% if isBot %}<p class="warning">Bot: {{ botType }}</p>{% endif %}`,
    Verdict:  verdict,
    Vars:     map[string]string{"locale": "en-GB"},
})
if err == nil && rendered.Success {
    fmt.Fprint(w, rendered.HTML)
}

Samma syntax om du ska ringa RenderWidget från Go, Node , eller använda <sb-widget> i webbläsaren: samma flytande motor , samma variabler som namnen på

Utformning

graph LR
    classDef input fill:none,stroke:#3b82f6,stroke-width:2px
    classDef async fill:none,stroke:#a855f7,stroke-width:2px
    classDef good fill:none,stroke:#22c55e,stroke-width:2px
    classDef store fill:none,stroke:#f59e0b,stroke-width:2px

    INT([Internet])
    CF["Cloudflare<br/>Tunnel / CDN"]
    CA["Caddy<br/>+ caddy-stylobot"]:::input
    SD["StyloBot Sidecar<br/>:5090 gRPC  ·  :5091 REST"]:::async
    WEB["Upstream App<br/>Node / Go / ASP.NET"]:::good
    DB[("SQLite<br/>sessions · reputation")]:::store

    INT --> CF --> CA
    CA -->|"gRPC Detect<br/>≤50ms"| SD
    SD <-->|"persist"| DB
    CA -->|"X-StyloBot-* headers"| WEB
    WEB -->|"/_stylobot/partials/render<br/>(widget rendering)"| SD

Applikationen uppströms ropar på sidecaren direkt för att göra widget rendering, genomgå vägarna. Widget rendering behöver fulla bedömande sammanhang och händer efter att applikationen redan passerat vägarnadetektorn , så det finns inga detektorduplikeringarM SK3

Misslyckande-öppna vid varje lager

Caddy plugin, Node middleware, och Go SDK alla misslyckas med att öppna : en bilstolstimmar eller fel blir en varningslog och ett tillåtande tomt slutsats M SK3 inte en MSC4xxM SK5 | | Caddy deadline är en kall | - | start safety margin | ; | den stabila |- | kostnaden på ett varmt anslutande är |

Världshandeln- -koder detta- -blockar den legitima trafiken, eftersom det inte är möjligt att upptäcka- -är värre än att sakna robottrafik under en bilolycka.


Veröffentlichungsserien fortsätter. Fler artiklar om detektionsinfrastrukturer , användningsmönster M SK2 observabilitet , och den kommersiella topologin är fortfarande kvar

källan till implementeringen: github.com/scottgalM SK2stylobot. Livmotorn , brädan, , och kommersiella kontrollen stylobot.net.

logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.