mirror of
https://github.com/Velocidex/velociraptor.git
synced 2026-10-12 02:09:59 +00:00
While the pool client is designed for _server_ load testing, it has shown itself to be useful for testing/learning/development scenarios too (for example, testing hunt behaviours). This PR preserves the load-testing aspects but adds some features that make the pool client more human-friendly and therefore more useful for lab and learning environments. Each pool client now presents a unique hostname and FQDN (e.g. "quantum-phoenix.example.com") to the server instead of the real host's hostname suffixed with the client id. The FQDN reuses the domain of the host running the pool client so the emulated clients appear to belong to the same network. Hostnames are adjective-noun combinations (e.g. "quantum-phoenix") - a numeric suffix is only appended when the number of clients is almost ten thousand. Name collisions are possible if running pool clients simultaneously on multiple hosts connected to the same server, but the server is designed to expect name collisions in the real world, so it's not an issue. Identities are generated upfront for all clients and applied to the `BasicInformation` and `DetailedInfo` sources of the `Generic.Client.Info` interrogation, as well as the periodic client info messages. Writebacks now default to in-memory only (`memory:// scheme`) so no writeback files are written to disk and each client's identity is ephemeral. This makes it easy to spin up clients -> run some tests -> stop the clients -> delete the data -> repeat, without needing to go clean up hundreds of writeback files each time. This also makes it easy to create massive amounts of hunt data in batches (create hunt -> add 10,000 clients -> hunt collections run -> stop clients -> add 10,000 new clients -> hunt collections run -> stop clients -> repeat) for server-side query performance testing. The writeback location is now configured for all platforms - previously Darwin was left unset and fell back to the config file's path, causing all clients on macOS to share a single writeback file. The root config loader no longer requires a writeback file, but it is still available if you need persistent pool client identities. Screenshots: <img width="1600" height="800" alt="pool_client" src="https://github.com/user-attachments/assets/53d14917-4252-4924-857c-acee5c6708eb" /> <img width="1200" height="637" alt="pool_client2" src="https://github.com/user-attachments/assets/c42300ec-4869-465b-b4d6-2141dcbca6bd" />
125 lines
4.5 KiB
Go
125 lines
4.5 KiB
Go
package executor
|
|
|
|
import (
|
|
"fmt"
|
|
"math/rand"
|
|
"strings"
|
|
)
|
|
|
|
// PoolIdentity represents the emulated identity of a pool client.
|
|
// Each pool client presents a unique hostname and FQDN to the server
|
|
// while all other client information (OS, platform, architecture,
|
|
// interfaces etc.) reflects the real host.
|
|
type PoolIdentity struct {
|
|
Hostname string
|
|
Fqdn string
|
|
}
|
|
|
|
// Word lists used to generate codename-style hostnames in the style
|
|
// of adjective-noun combinations (e.g. "brave-falcon",
|
|
// "cosmic-orbit"). The lists are deliberately platform agnostic so
|
|
// the generated names look plausible on any operating system.
|
|
var (
|
|
pool_adjectives = []string{
|
|
"brave", "swift", "cosmic", "golden", "silver", "crimson",
|
|
"emerald", "azure", "violet", "amber", "mystic", "silent",
|
|
"raging", "gentle", "fierce", "noble", "royal", "ancient",
|
|
"modern", "digital", "quantum", "stellar", "lunar", "solar",
|
|
"shadowy", "phantom", "thunderous", "stormy", "frosty",
|
|
"blazing", "radiant", "hidden", "secret", "sacred", "wild",
|
|
"calm", "bright", "dark", "deep", "quick", "strong", "wise",
|
|
"lucky", "happy", "sunny", "misty", "rocky", "sandy", "grassy",
|
|
"woody", "icy", "fiery", "electric", "magnetic", "atomic",
|
|
"orbital", "galactic", "nebular", "prismatic", "echoing",
|
|
"pulsing",
|
|
"cyber", "neon", "plasma", "photon", "binary", "chrome",
|
|
"silicon", "fractal", "vector", "pixel", "glitch", "static",
|
|
"sonic", "laser", "turbo", "hyper", "stealth", "tactical",
|
|
"infinite", "eternal", "arcane", "runic", "eldritch", "null",
|
|
"void", "zero", "temporal", "kinetic", "synthetic",
|
|
"holographic",
|
|
}
|
|
|
|
pool_nouns = []string{
|
|
"falcon", "eagle", "hawk", "raven", "owl", "wolf", "fox",
|
|
"bear", "lion", "tiger", "panther", "leopard", "jaguar",
|
|
"cheetah", "lynx", "puma", "cobra", "viper", "python",
|
|
"dragon", "phoenix", "griffin", "unicorn", "pegasus", "hydra",
|
|
"kraken", "leviathan", "behemoth", "colossus", "titan",
|
|
"wizard", "knight", "samurai", "ninja", "ranger", "hunter",
|
|
"scout", "sentinel", "guardian", "defender", "warrior",
|
|
"champion", "hero", "legend", "myth", "saga", "odyssey",
|
|
"voyage", "journey", "quest", "compass", "beacon",
|
|
"lighthouse", "anchor", "horizon", "summit", "peak", "ridge",
|
|
"canyon", "river", "ocean",
|
|
"robot", "android", "cyborg", "droid", "mech",
|
|
"singularity", "wormhole", "starship", "rocket", "probe",
|
|
"comet", "meteor", "asteroid", "nebula", "galaxy", "eclipse",
|
|
"aurora", "quasar", "pulsar", "nova", "zenith", "kernel",
|
|
"daemon", "proxy", "cipher", "firewall", "matrix", "node",
|
|
"basilisk", "chimera", "cerberus", "wyvern", "drake", "golem",
|
|
"paladin", "berserker", "gladiator", "archer", "renegade",
|
|
"nomad",
|
|
}
|
|
)
|
|
|
|
// GeneratePoolIdentities returns count unique identities for pool
|
|
// clients. Hostnames are generated as adjective-noun combinations
|
|
// (e.g. "brave-falcon"). If the number of clients exceeds the number
|
|
// of available combinations a random numeric suffix is appended to
|
|
// keep every hostname unique.
|
|
//
|
|
// The FQDN reuses the domain portion of the real host's FQDN so the
|
|
// emulated clients appear to belong to the same network as the host
|
|
// running the pool client (e.g. "brave-falcon.example.com" when the
|
|
// host is "myhost.example.com").
|
|
func GeneratePoolIdentities(count int, real_fqdn string) []PoolIdentity {
|
|
domain := deriveDomain(real_fqdn)
|
|
used := make(map[string]bool)
|
|
result := make([]PoolIdentity, 0, count)
|
|
|
|
for i := 0; i < count; i++ {
|
|
hostname := generateUniqueHostname(used)
|
|
result = append(result, PoolIdentity{
|
|
Hostname: hostname,
|
|
Fqdn: hostname + "." + domain,
|
|
})
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// deriveDomain extracts the domain portion of a fully qualified
|
|
// domain name. For example "myhost.example.com" returns
|
|
// "example.com". If the FQDN has no domain portion (e.g. a bare
|
|
// hostname) we fall back to "local" so generated FQDNs always look
|
|
// like proper domain names.
|
|
func deriveDomain(real_fqdn string) string {
|
|
parts := strings.SplitN(real_fqdn, ".", 2)
|
|
if len(parts) == 2 && parts[1] != "" {
|
|
return parts[1]
|
|
}
|
|
return "local"
|
|
}
|
|
|
|
func generateUniqueHostname(used map[string]bool) string {
|
|
for {
|
|
hostname := fmt.Sprintf("%s-%s",
|
|
pool_adjectives[rand.Intn(len(pool_adjectives))],
|
|
pool_nouns[rand.Intn(len(pool_nouns))])
|
|
|
|
if !used[hostname] {
|
|
used[hostname] = true
|
|
return hostname
|
|
}
|
|
|
|
// The base combination is already taken - disambiguate with a
|
|
// random numeric suffix.
|
|
hostname = fmt.Sprintf("%s-%d", hostname, rand.Intn(999)+1)
|
|
if !used[hostname] {
|
|
used[hostname] = true
|
|
return hostname
|
|
}
|
|
}
|
|
}
|