Full-stack FFI: sentinel-core Rust + Shield C linked via CGo, production Dockerfile + deploy script

This commit is contained in:
DmitrL-dev 2026-03-23 17:08:41 +10:00
parent 41cbfd6e0a
commit d71ada8977
2 changed files with 270 additions and 95 deletions

View file

@ -3,36 +3,25 @@
package engines package engines
/* /*
#cgo LDFLAGS: -L${SRCDIR}/../../../../sentinel-core/target/release -lsentinel_core #cgo LDFLAGS: -L${SRCDIR}/../../../../sentinel-core/target/release -lsentinel_core -ldl -lm -lpthread
#cgo CFLAGS: -I${SRCDIR}/../../../../sentinel-core/include #cgo CFLAGS: -I${SRCDIR}/../../../../sentinel-core/include
// sentinel_core.h — C-compatible FFI interface for Rust sentinel-core. #include <sentinel_core.h>
// These declarations match the Rust #[no_mangle] extern "C" functions. #include <stdlib.h>
//
// Build sentinel-core:
// cd sentinel-core && cargo build --release
//
// The library exposes:
// sentinel_init() — Initialize the engine
// sentinel_analyze() — Analyze text for jailbreak/injection patterns
// sentinel_status() — Get engine health status
// sentinel_shutdown() — Graceful shutdown
// Stub declarations for build without native library.
// When building WITH sentinel-core, replace stubs with actual FFI.
*/ */
import "C" import "C"
import ( import (
"context"
"encoding/json"
"fmt"
"sync" "sync"
"time" "time"
"unsafe"
) )
// NativeSentinelCore wraps the Rust sentinel-core via CGo FFI. // NativeSentinelCore wraps the Rust sentinel-core via CGo FFI.
// Build tag: sentinel_native // Build tag: sentinel_native
//
// When sentinel-core.so/dylib is not available, the StubSentinelCore
// is used automatically (see engines.go).
type NativeSentinelCore struct { type NativeSentinelCore struct {
mu sync.RWMutex mu sync.RWMutex
initialized bool initialized bool
@ -40,45 +29,99 @@ type NativeSentinelCore struct {
lastCheck time.Time lastCheck time.Time
} }
// NewNativeSentinelCore creates the FFI bridge. // NewNativeSentinelCore creates the FFI bridge and initializes the Rust engine.
// Returns error if the native library is not available.
func NewNativeSentinelCore() (*NativeSentinelCore, error) { func NewNativeSentinelCore() (*NativeSentinelCore, error) {
n := &NativeSentinelCore{ result := C.sentinel_init()
version: "0.1.0-ffi", if result != 0 {
return nil, fmt.Errorf("sentinel_init failed with code %d", int(result))
} }
// TODO: Call C.sentinel_init() when native library is available // Get version from Rust
// result := C.sentinel_init() cVer := C.sentinel_version()
// if result != 0 { version := "unknown"
// return nil, fmt.Errorf("sentinel_init failed: %d", result) if cVer != nil {
// } version = C.GoString(cVer)
C.sentinel_free(cVer)
}
n.initialized = true return &NativeSentinelCore{
n.lastCheck = time.Now() initialized: true,
return n, nil version: version,
lastCheck: time.Now(),
}, nil
} }
// Analyze sends text through the sentinel-core analysis pipeline. // sentinelAnalyzeResult matches the JSON returned by sentinel_analyze().
// Returns: confidence (0-1), detected categories, is_threat flag. type sentinelAnalyzeResult struct {
func (n *NativeSentinelCore) Analyze(text string) SentinelResult { Confidence float64 `json:"confidence"`
Categories []string `json:"categories"`
IsThreat bool `json:"is_threat"`
InputLength int `json:"input_length"`
AnalyzeCount uint64 `json:"analyze_count"`
Error string `json:"error,omitempty"`
}
// analyze sends text through the Rust sentinel-core analysis pipeline.
func (n *NativeSentinelCore) analyze(text string) sentinelAnalyzeResult {
n.mu.RLock() n.mu.RLock()
defer n.mu.RUnlock() defer n.mu.RUnlock()
if !n.initialized { if !n.initialized {
return SentinelResult{Error: "engine not initialized"} return sentinelAnalyzeResult{Error: "engine not initialized"}
} }
// TODO: FFI call cText := C.CString(text)
// cText := C.CString(text) defer C.free(unsafe.Pointer(cText))
// defer C.free(unsafe.Pointer(cText))
// result := C.sentinel_analyze(cText)
// Stub analysis for now cResult := C.sentinel_analyze(cText)
return SentinelResult{ if cResult == nil {
Confidence: 0.0, return sentinelAnalyzeResult{Error: "sentinel_analyze returned null"}
Categories: []string{},
IsThreat: false,
} }
defer C.sentinel_free(cResult)
jsonStr := C.GoString(cResult)
var result sentinelAnalyzeResult
if err := json.Unmarshal([]byte(jsonStr), &result); err != nil {
return sentinelAnalyzeResult{Error: fmt.Sprintf("json parse error: %v", err)}
}
return result
}
// ScanPrompt analyzes an LLM prompt for injection/jailbreak patterns.
func (n *NativeSentinelCore) ScanPrompt(_ context.Context, prompt string) (*ScanResult, error) {
start := time.Now()
res := n.analyze(prompt)
if res.Error != "" {
return nil, fmt.Errorf("sentinel-core: %s", res.Error)
}
severity := "NONE"
threatType := ""
if res.IsThreat {
severity = "HIGH"
if len(res.Categories) > 0 {
threatType = res.Categories[0]
}
}
return &ScanResult{
Engine: "sentinel-core",
ThreatFound: res.IsThreat,
ThreatType: threatType,
Severity: severity,
Confidence: res.Confidence,
Details: fmt.Sprintf("categories=%v", res.Categories),
Indicators: res.Categories,
Duration: time.Since(start),
Timestamp: time.Now(),
}, nil
}
// ScanResponse analyzes an LLM response for data exfiltration or harmful content.
func (n *NativeSentinelCore) ScanResponse(ctx context.Context, response string) (*ScanResult, error) {
return n.ScanPrompt(ctx, response)
} }
// Status returns the engine health via FFI. // Status returns the engine health via FFI.
@ -90,8 +133,27 @@ func (n *NativeSentinelCore) Status() EngineStatus {
return EngineOffline return EngineOffline
} }
// TODO: Call C.sentinel_status() cStatus := C.sentinel_status()
return EngineHealthy if cStatus == nil {
return EngineDegraded
}
defer C.sentinel_free(cStatus)
var statusObj struct {
Status string `json:"status"`
}
if err := json.Unmarshal([]byte(C.GoString(cStatus)), &statusObj); err != nil {
return EngineDegraded
}
switch statusObj.Status {
case "HEALTHY":
return EngineHealthy
case "OFFLINE":
return EngineOffline
default:
return EngineDegraded
}
} }
// Name returns the engine identifier. // Name returns the engine identifier.
@ -109,15 +171,14 @@ func (n *NativeSentinelCore) Shutdown() error {
n.mu.Lock() n.mu.Lock()
defer n.mu.Unlock() defer n.mu.Unlock()
// TODO: C.sentinel_shutdown() if !n.initialized {
return nil
}
result := C.sentinel_shutdown()
n.initialized = false n.initialized = false
if result != 0 {
return fmt.Errorf("sentinel_shutdown failed with code %d", int(result))
}
return nil return nil
} }
// SentinelResult is returned by the Analyze function.
type SentinelResult struct {
Confidence float64 `json:"confidence"`
Categories []string `json:"categories"`
IsThreat bool `json:"is_threat"`
Error string `json:"error,omitempty"`
}

View file

@ -3,68 +3,163 @@
package engines package engines
/* /*
#cgo LDFLAGS: -L${SRCDIR}/../../../../shield/build -lshield #cgo LDFLAGS: -L${SRCDIR}/../../../../shield/build -lsentinel-shield -lstdc++ -lm -lpthread
#cgo CFLAGS: -I${SRCDIR}/../../../../shield/include #cgo CFLAGS: -I${SRCDIR}/../../../../shield/include
// shield.h — C-compatible FFI interface for C++ shield engine. #include <stdlib.h>
// These declarations match the extern "C" functions from shield.
// // Shield C FFI exports
// Build shield: extern int shield_init(void);
// cd shield && mkdir build && cd build && cmake .. && make extern char* shield_inspect(const char* payload, int payload_len);
// extern char* shield_status(void);
// The library exposes: extern int shield_shutdown(void);
// shield_init() — Initialize the network protection engine extern void shield_free(char* ptr);
// shield_inspect() — Deep packet inspection / prompt filtering
// shield_status() — Get engine health
// shield_shutdown() — Graceful shutdown
*/ */
import "C" import "C"
import ( import (
"context"
"encoding/json"
"fmt"
"sync" "sync"
"time" "time"
"unsafe"
) )
// NativeShield wraps the C++ shield engine via CGo FFI. // NativeShield wraps the C Shield engine via CGo FFI.
// Build tag: shield_native // Build tag: shield_native
type NativeShield struct { type NativeShield struct {
mu sync.RWMutex mu sync.RWMutex
initialized bool initialized bool
version string version string
lastCheck time.Time lastCheck time.Time
blocked []BlockedIP // In-memory block list
} }
// NewNativeShield creates the FFI bridge to the C++ shield engine. // NewNativeShield creates the FFI bridge to the C Shield engine.
func NewNativeShield() (*NativeShield, error) { func NewNativeShield() (*NativeShield, error) {
n := &NativeShield{ result := C.shield_init()
version: "0.1.0-ffi", if result != 0 {
return nil, fmt.Errorf("shield_init failed with code %d", int(result))
} }
// TODO: Call C.shield_init() // Get version from status
n.initialized = true version := "0.1.0"
n.lastCheck = time.Now() cStatus := C.shield_status()
return n, nil if cStatus != nil {
var statusObj struct {
Version string `json:"version"`
}
if err := json.Unmarshal([]byte(C.GoString(cStatus)), &statusObj); err == nil && statusObj.Version != "" {
version = statusObj.Version
}
C.shield_free(cStatus)
}
return &NativeShield{
initialized: true,
version: version,
lastCheck: time.Now(),
blocked: make([]BlockedIP, 0),
}, nil
} }
// Inspect runs deep packet inspection on the payload. // shieldInspectResult matches the JSON returned by shield_inspect().
func (n *NativeShield) Inspect(payload []byte) ShieldResult { type shieldInspectResult struct {
Blocked bool `json:"blocked"`
Reason string `json:"reason"`
Confidence float64 `json:"confidence"`
InspectCount string `json:"inspect_count,omitempty"`
Error string `json:"error,omitempty"`
}
// inspect sends payload through the C Shield inspection pipeline.
func (n *NativeShield) inspect(payload []byte) shieldInspectResult {
n.mu.RLock() n.mu.RLock()
defer n.mu.RUnlock() defer n.mu.RUnlock()
if !n.initialized { if !n.initialized {
return ShieldResult{Error: "engine not initialized"} return shieldInspectResult{Error: "engine not initialized"}
} }
// TODO: FFI call cPayload := C.CBytes(payload)
// cPayload := C.CBytes(payload) defer C.free(cPayload)
// defer C.free(cPayload)
// result := C.shield_inspect((*C.char)(cPayload), C.int(len(payload)))
return ShieldResult{ cResult := C.shield_inspect((*C.char)(cPayload), C.int(len(payload)))
Blocked: false, if cResult == nil {
Reason: "", return shieldInspectResult{Error: "shield_inspect returned null"}
Confidence: 0.0,
} }
defer C.shield_free(cResult)
jsonStr := C.GoString(cResult)
var result shieldInspectResult
if err := json.Unmarshal([]byte(jsonStr), &result); err != nil {
return shieldInspectResult{Error: fmt.Sprintf("json parse error: %v", err)}
}
return result
}
// InspectTraffic analyzes network traffic / payload for threats.
func (n *NativeShield) InspectTraffic(_ context.Context, payload []byte, metadata map[string]string) (*ScanResult, error) {
start := time.Now()
res := n.inspect(payload)
if res.Error != "" {
return nil, fmt.Errorf("shield: %s", res.Error)
}
severity := "NONE"
threatType := ""
if res.Blocked {
severity = "CRITICAL"
threatType = "network_threat"
}
details := res.Reason
if src, ok := metadata["source_ip"]; ok {
details += fmt.Sprintf(" (from %s)", src)
}
return &ScanResult{
Engine: "shield",
ThreatFound: res.Blocked,
ThreatType: threatType,
Severity: severity,
Confidence: res.Confidence,
Details: details,
Duration: time.Since(start),
Timestamp: time.Now(),
}, nil
}
// BlockIP adds an IP to the in-memory block list.
func (n *NativeShield) BlockIP(_ context.Context, ip string, reason string, duration time.Duration) error {
n.mu.Lock()
defer n.mu.Unlock()
n.blocked = append(n.blocked, BlockedIP{
IP: ip,
Reason: reason,
BlockedAt: time.Now(),
ExpiresAt: time.Now().Add(duration),
})
return nil
}
// ListBlocked returns currently blocked IPs (filters expired).
func (n *NativeShield) ListBlocked(_ context.Context) ([]BlockedIP, error) {
n.mu.RLock()
defer n.mu.RUnlock()
now := time.Now()
active := make([]BlockedIP, 0, len(n.blocked))
for _, b := range n.blocked {
if b.ExpiresAt.After(now) {
active = append(active, b)
}
}
return active, nil
} }
// Status returns the engine health via FFI. // Status returns the engine health via FFI.
@ -76,7 +171,27 @@ func (n *NativeShield) Status() EngineStatus {
return EngineOffline return EngineOffline
} }
return EngineHealthy cStatus := C.shield_status()
if cStatus == nil {
return EngineDegraded
}
defer C.shield_free(cStatus)
var statusObj struct {
Status string `json:"status"`
}
if err := json.Unmarshal([]byte(C.GoString(cStatus)), &statusObj); err != nil {
return EngineDegraded
}
switch statusObj.Status {
case "HEALTHY":
return EngineHealthy
case "OFFLINE":
return EngineOffline
default:
return EngineDegraded
}
} }
// Name returns the engine identifier. // Name returns the engine identifier.
@ -94,15 +209,14 @@ func (n *NativeShield) Shutdown() error {
n.mu.Lock() n.mu.Lock()
defer n.mu.Unlock() defer n.mu.Unlock()
// TODO: C.shield_shutdown() if !n.initialized {
return nil
}
result := C.shield_shutdown()
n.initialized = false n.initialized = false
if result != 0 {
return fmt.Errorf("shield_shutdown failed with code %d", int(result))
}
return nil return nil
} }
// ShieldResult is returned by the Inspect function.
type ShieldResult struct {
Blocked bool `json:"blocked"`
Reason string `json:"reason,omitempty"`
Confidence float64 `json:"confidence"`
Error string `json:"error,omitempty"`
}