Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions .env.secret.example
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
# .env.secret — sensitive values for docker-compose.k8s.yml
# .env.secret — sensitive values for docker-compose
#
# !! DO NOT COMMIT THIS FILE TO GIT !!
# Add to .gitignore: echo ".env.secret" >> .gitignore
#
# Copy this file to .env.secret and fill in real values.
# Passwords below match the hashes in docker/config/vcache.acl.

# VoidCache password (leave blank to disable auth)
VC_PASSWORD=
# VoidCache passwords (must match hashes in vcache.acl)
VC_PASSWORD_ADMIN=adminpass
VC_PASSWORD_APP=apppass
VC_PASSWORD_READER=readonly
VC_PASSWORD_DEFAULT=secret

# Simple single-password mode (overrides ACL; leave blank to use ACL file)
# VC_PASSWORD=

# Registry credentials (only needed for private registries)
# REGISTRY=ghcr.io/yourorg
Expand Down
36 changes: 36 additions & 0 deletions BUILD.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
## 1. Create the secrets file

```bash
# In PowerShell — run from the project root
# Leave VC_PASSWORD blank to use the ACL file instead
Copy-Item docker\.env docker\.env.local
```
The ACL file at `docker\config\vcache.acl`
already has the correct credentials.
You don't need to set VC_PASSWORD unless you want simple password-only mode.

## 2. Build and start

```bash
# Open PowerShell in the project root folder
# (right-click the folder → "Open in Terminal")

docker compose -f docker\docker-compose.yml up --build -d
```
First build takes 2–3 minutes (downloads Ubuntu base, compiles C source). Subsequent builds are ~20 seconds.

3. Check that all containers are healthy
```bash
docker compose -f docker\docker-compose.yml ps
```

Wait ~30 seconds after start. All four containers (vcache-1, vcache-2, vcache-3, vcache-haproxy) should show healthy or running.

Connect on localhost:6379 (HAProxy) — this is what StackExchange.Redis and RedisInsight should point at.
```
Port map
Port What Who connects here
6379 HAProxy — main entry point Your app RedisInsight redis-cli
8404 HAProxy stats UI Browser → http://localhost:8404/stats (user: admin / pass: admin)
6381 vcache-0 /
```
3 changes: 2 additions & 1 deletion docker/.env
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ VC_MAXMEMORY=512m
VC_THREADS=4

# Simple password — set to enable AUTH on all nodes (leave blank to disable)
# VC_PASSWORD=changeme_in_production
# VC_PASSWORD=secret # matches 'default' user in vcache.acl
# VC_PASSWORD_ADMIN=adminpass # matches 'admin' user in vcache.acl

# Set to "1" to run nodes without WAL persistence (faster, no durability)
VC_NO_WAL=0
22 changes: 22 additions & 0 deletions docker/.env.local
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# .env – VoidCache Docker environment defaults
#
# Copy to .env and edit before running docker compose up.
# All values can also be passed as shell environment variables.
#
# ─────────────────────────────────────────────────────────────────────────────

# Port exposed on the host for the HAProxy load balancer
VC_PORT=6379

# Memory cap per node (supports k, m, g suffixes)
VC_MAXMEMORY=512m

# Worker threads per node (set to CPU cores for best perf)
VC_THREADS=4

# Simple password — set to enable AUTH on all nodes (leave blank to disable)
# VC_PASSWORD=secret # matches 'default' user in vcache.acl
# VC_PASSWORD_ADMIN=adminpass # matches 'admin' user in vcache.acl

# Set to "1" to run nodes without WAL persistence (faster, no durability)
VC_NO_WAL=0
6 changes: 3 additions & 3 deletions docker/config/vcache.acl
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@
# Example users:
#
# Full-access admin (password: "adminpass")
admin 3ba05beee8a9d6c6c5e99bf49e6a17eb6d7f9a5ad5b5bfc14f7c5b0a8d1f2c3 *
admin 713bfda78870bf9d1b261f565286f85e97ee614efe5f0faf7c34e7ca4f65baca *
#
# Read-only service account (password: "readonly")
reader e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 r
reader 8171bacf32668a8f44b90087ad107ed63170f57154763ba7e44047bf9e5a7be3 r
#
# Application account with read+write (password: "apppass")
app 5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8 rw
app 46400f642c99584e51b14031f875ca9b7b33ccf94ff8d7657197a5bac06f4ffb rw
#
# Default user (no-username AUTH <password>) password: "secret"
default 2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b rw
188 changes: 182 additions & 6 deletions net/commands.c
Original file line number Diff line number Diff line change
Expand Up @@ -157,21 +157,46 @@ static void cmd_quit(vcserver_t *srv, vc_conn_t *conn, vc_cmd_t *cmd) {
conn->state = VC_CONN_CLOSING;
}

/* RESET — resets connection state. StackExchange.Redis sends this on reconnect. */
static void cmd_reset(vcserver_t *srv, vc_conn_t *conn, vc_cmd_t *cmd) {
(void)srv; (void)cmd;
conn->db_idx = 0;
resp_write_simple(WBUF(conn), "RESET");
}

static void cmd_client(vcserver_t *srv, vc_conn_t *conn, vc_cmd_t *cmd) {
(void)srv;
if (cmd->argc < 2) { ERR(conn, "ERR", "wrong arguments"); return; }
const char *sub = cmd->argv[1];
if (strcasecmp(sub, "SETNAME") == 0) { OK(conn); return; }
if (strcasecmp(sub, "GETNAME") == 0) { BULK(conn, "", 0); return; }
if (strcasecmp(sub, "ID") == 0) { INT(conn, (int64_t)conn->fd); return; }
if (strcasecmp(sub, "SETNAME") == 0) { OK(conn); return; }
if (strcasecmp(sub, "GETNAME") == 0) { BULK(conn, "", 0); return; }
if (strcasecmp(sub, "ID") == 0) { INT(conn, (int64_t)conn->fd); return; }
/* StackExchange.Redis / RedisInsight no-op stubs */
if (strcasecmp(sub, "NO-EVICT") == 0) { OK(conn); return; }
if (strcasecmp(sub, "NO-TOUCH") == 0) { OK(conn); return; }
if (strcasecmp(sub, "UNPAUSE") == 0) { OK(conn); return; }
if (strcasecmp(sub, "PAUSE") == 0) { OK(conn); return; }
if (strcasecmp(sub, "CACHING") == 0) { OK(conn); return; }
if (strcasecmp(sub, "REPLY") == 0) { OK(conn); return; }
if (strcasecmp(sub, "TRACKING") == 0) { OK(conn); return; }
if (strcasecmp(sub, "INFO") == 0) {
char info[256];
snprintf(info, sizeof(info),
"id=%d\r\naddr=%s:%u\r\ncmd=client\r\n",
conn->fd, conn->peer_addr, conn->peer_port);
BULK(conn, info, strlen(info)); return;
}
ERR(conn, "ERR", "unknown CLIENT subcommand");
if (strcasecmp(sub, "LIST") == 0) {
/* Return minimal CLIENT LIST entry for this connection */
char buf[512];
int n = snprintf(buf, sizeof(buf),
"id=%d addr=%s:%u fd=%d name= age=0 idle=0 flags=N "
"db=0 sub=0 psub=0 multi=-1 rbs=16384 rbp=0 "
"obl=0 oll=0 omem=0 events=r cmd=client resp=2\n",
conn->fd, conn->peer_addr, conn->peer_port, conn->fd);
BULK(conn, buf, (size_t)n); return;
}
resp_write_error(WBUF(conn), "ERR", "unknown CLIENT subcommand");
}

/* ══════════════════════════════════════════════════════════════
Expand Down Expand Up @@ -583,6 +608,8 @@ static void cmd_info(vcserver_t *srv, vc_conn_t *conn, vc_cmd_t *cmd) {
char info[4096];
int n = snprintf(info, sizeof(info),
"# Server\r\n"
"redis_version:%s\r\n"
"redis_mode:%s\r\n"
"server:voidcache\r\n"
"version:%s\r\n"
"node_id:%s\r\n"
Expand All @@ -604,10 +631,29 @@ static void cmd_info(vcserver_t *srv, vc_conn_t *conn, vc_cmd_t *cmd) {
"# Keyspace\r\n"
"db0:keys=%llu\r\n"
"\r\n"
"# Replication\r\n"
"role:master\r\n"
"connected_slaves:0\r\n"
"master_failover_state:no-failover\r\n"
"master_replid:0000000000000000000000000000000000000000\r\n"
"master_repl_offset:0\r\n"
"\r\n"
"# Persistence\r\n"
"loading:0\r\n"
"rdb_changes_since_last_save:0\r\n"
"rdb_last_bgsave_status:ok\r\n"
"aof_enabled:0\r\n"
"\r\n"
"# CPU\r\n"
"used_cpu_sys:0\r\n"
"used_cpu_user:0\r\n"
"\r\n"
"# Memory\r\n"
"used_memory:%llu\r\n"
"maxmemory:%llu\r\n",
VC_SERVER_VERSION,
srv->cfg.cluster_enabled ? "cluster" : "standalone",
VC_SERVER_VERSION,
srv->node_id,
srv->cfg.cluster_enabled ? "cluster" : "standalone",
(unsigned long long)atomic_load(&srv->total_conns),
Expand All @@ -629,8 +675,44 @@ static void cmd_config(vcserver_t *srv, vc_conn_t *conn, vc_cmd_t *cmd) {
(void)srv;
if (cmd->argc < 2) { ERR(conn, "ERR", "wrong arguments"); return; }
if (strcasecmp(cmd->argv[1], "GET") == 0 && cmd->argc >= 3) {
/* Return empty array — no config params exposed */
resp_write_array_header(WBUF(conn), 0);
/* Return a minimal but realistic config for driver compatibility.
* StackExchange.Redis and RedisInsight query these on connect. */
const char *pat = cmd->argv[2];
/* Helper: does the glob pattern match the key? (simple * wildcard) */
#define CFGMATCH(k) (strcmp(pat,"*")==0 || strcasecmp(pat,(k))==0 || (pat[strlen(pat)-1]=='*' && strncasecmp(pat,(k),strlen(pat)-1)==0))
char maxmem[32];
snprintf(maxmem, sizeof(maxmem), "%llu",
(unsigned long long)srv->cache->max_memory);
/* Build array of matched key-value pairs */
struct { const char *k; const char *v; } entries[] = {
{ "hz", "10" },
{ "maxmemory", maxmem },
{ "maxmemory-policy", "allkeys-lru"},
{ "save", "" },
{ "appendonly", "no" },
{ "list-max-ziplist-size", "-2" },
{ "list-compress-depth", "0" },
{ "activerehashing", "yes" },
{ "lazyfree-lazy-eviction", "no" },
{ "latency-tracking", "yes" },
{ "latency-tracking-info-percentiles", "50 99 99.9" },
{ "proto-max-bulk-len", "536870912" },
{ "activedefrag", "no" },
{ "repl-backlog-size", "1048576" },
{ "bind-source-addr", "" },
{ "socket-mark-id", "0" },
};
int nmatch = 0;
for (size_t e = 0; e < sizeof(entries)/sizeof(entries[0]); e++)
if (CFGMATCH(entries[e].k)) nmatch++;
resp_write_array_header(WBUF(conn), nmatch * 2);
for (size_t e = 0; e < sizeof(entries)/sizeof(entries[0]); e++) {
if (CFGMATCH(entries[e].k)) {
BULK(conn, entries[e].k, strlen(entries[e].k));
BULK(conn, entries[e].v, strlen(entries[e].v));
}
}
#undef CFGMATCH
return;
}
if (strcasecmp(cmd->argv[1], "SET") == 0) { OK(conn); return; }
Expand Down Expand Up @@ -885,6 +967,91 @@ typedef struct {
uint32_t acl_needed; /* 0 = always allowed (PING/AUTH/HELLO) */
} vc_cmd_entry_t;

/* ── ACL commands (RedisInsight + driver feature detection) ────────────── */
static void cmd_acl(vcserver_t *srv, vc_conn_t *conn, vc_cmd_t *cmd) {
(void)srv;
if (cmd->argc < 2) { ERR(conn, "ERR", "wrong arguments"); return; }
const char *sub = cmd->argv[1];
if (strcasecmp(sub, "WHOAMI") == 0) {
const char *name = (conn->user && conn->user->username[0])
? conn->user->username : "default";
BULK(conn, name, strlen(name)); return;
}
if (strcasecmp(sub, "CAT") == 0) {
resp_write_array_header(WBUF(conn), 4);
BULK(conn, "read", 4); BULK(conn, "write", 5);
BULK(conn, "admin", 5); BULK(conn, "pubsub", 6); return;
}
if (strcasecmp(sub, "LIST") == 0) {
resp_write_array_header(WBUF(conn), 1);
const char *name = (conn->user && conn->user->username[0])
? conn->user->username : "default";
char rule[128];
snprintf(rule, sizeof(rule), "user %s on ~* &* +@all", name);
BULK(conn, rule, strlen(rule)); return;
}
if (strcasecmp(sub, "GETUSER") == 0) { resp_write_null_compat(WBUF(conn), conn->resp3); return; }
if (strcasecmp(sub, "SETUSER") == 0 ||
strcasecmp(sub, "DELUSER") == 0 ||
strcasecmp(sub, "LOG") == 0 ||
strcasecmp(sub, "RESET") == 0) { OK(conn); return; }
resp_write_error(WBUF(conn), "ERR", "unknown ACL subcommand");
}

/* ── LATENCY (RedisInsight monitoring) ─────────────────────────────────── */
static void cmd_latency(vcserver_t *srv, vc_conn_t *conn, vc_cmd_t *cmd) {
(void)srv;
if (cmd->argc < 2) { ERR(conn, "ERR", "wrong arguments"); return; }
const char *sub = cmd->argv[1];
if (strcasecmp(sub, "LATEST") == 0) { resp_write_array_header(WBUF(conn), 0); return; }
if (strcasecmp(sub, "HISTORY") == 0) { resp_write_array_header(WBUF(conn), 0); return; }
if (strcasecmp(sub, "RESET") == 0) { INT(conn, 0); return; }
if (strcasecmp(sub, "GRAPH") == 0) { BULK(conn, "", 0); return; }
resp_write_error(WBUF(conn), "ERR", "unknown LATENCY subcommand");
}

/* ── SLOWLOG (RedisInsight) ─────────────────────────────────────────────── */
static void cmd_slowlog(vcserver_t *srv, vc_conn_t *conn, vc_cmd_t *cmd) {
(void)srv;
if (cmd->argc < 2) { ERR(conn, "ERR", "wrong arguments"); return; }
const char *sub = cmd->argv[1];
if (strcasecmp(sub, "GET") == 0) { resp_write_array_header(WBUF(conn), 0); return; }
if (strcasecmp(sub, "LEN") == 0) { INT(conn, 0); return; }
if (strcasecmp(sub, "RESET") == 0) { OK(conn); return; }
resp_write_error(WBUF(conn), "ERR", "unknown SLOWLOG subcommand");
}

/* ── OBJECT (RedisInsight key inspector) ────────────────────────────────── */
static void cmd_object(vcserver_t *srv, vc_conn_t *conn, vc_cmd_t *cmd) {
(void)srv;
if (cmd->argc < 2) { ERR(conn, "ERR", "wrong arguments"); return; }
const char *sub = cmd->argv[1];
if (strcasecmp(sub, "ENCODING") == 0) { BULK(conn, "embstr", 7); return; }
if (strcasecmp(sub, "REFCOUNT") == 0) { INT(conn, 1); return; }
if (strcasecmp(sub, "IDLETIME") == 0) { INT(conn, 0); return; }
if (strcasecmp(sub, "FREQ") == 0) { INT(conn, 0); return; }
if (strcasecmp(sub, "HELP") == 0) { resp_write_array_header(WBUF(conn), 0); return; }
resp_write_error(WBUF(conn), "ERR", "unknown OBJECT subcommand");
}

/* ── WAIT (SE.Redis replication sync) ──────────────────────────────────── */
static void cmd_wait(vcserver_t *srv, vc_conn_t *conn, vc_cmd_t *cmd) {
(void)srv; (void)cmd;
INT(conn, 0); /* single-node: 0 replicas */
}

/* ── XLEN stub (Streams feature detection) ──────────────────────────────── */
static void cmd_xlen(vcserver_t *srv, vc_conn_t *conn, vc_cmd_t *cmd) {
(void)srv; (void)cmd;
ERR(conn, "WRONGTYPE", "Operation against a key holding the wrong kind of value");
}

/* ── SRANDMEMBER stub ───────────────────────────────────────────────────── */
static void cmd_srandmember(vcserver_t *srv, vc_conn_t *conn, vc_cmd_t *cmd) {
(void)srv; (void)cmd;
ERR(conn, "WRONGTYPE", "Operation against a key holding the wrong kind of value");
}

static const vc_cmd_entry_t cmd_table[] = {
/* Connection */
{ "PING", cmd_ping, 0 },
Expand Down Expand Up @@ -928,6 +1095,15 @@ static const vc_cmd_entry_t cmd_table[] = {
{ "VCSET", cmd_vcset, VC_ACL_WRITE },
{ "VCGET", cmd_vcget, VC_ACL_READ },
{ "VCINFO", cmd_vcinfo, VC_ACL_READ },
/* StackExchange.Redis + RedisInsight compatibility */
{ "RESET", cmd_reset, 0 },
{ "ACL", cmd_acl, 0 },
{ "LATENCY", cmd_latency, VC_ACL_ADMIN },
{ "SLOWLOG", cmd_slowlog, VC_ACL_ADMIN },
{ "OBJECT", cmd_object, VC_ACL_READ },
{ "WAIT", cmd_wait, 0 },
{ "XLEN", cmd_xlen, VC_ACL_READ },
{ "SRANDMEMBER", cmd_srandmember, VC_ACL_READ },
};

#define CMD_TABLE_SIZE (sizeof(cmd_table) / sizeof(cmd_table[0]))
Expand Down
22 changes: 16 additions & 6 deletions net/proto.c
Original file line number Diff line number Diff line change
Expand Up @@ -127,24 +127,34 @@ resp_parse_result_t resp_parse_command(const char *buf, size_t len,

#define NEED(n) do { if ((size_t)(end - p) < (size_t)(n)) return RESP_PARSE_MORE; } while(0)

/* ── Inline command (telnet-friendly) ── */
/* ── Inline command (telnet-friendly, handles \r\n and bare \n) ── */
if (*p != '*') {
const char *nl = find_crlf(p, (size_t)(end - p));
/* Find end of line — accept both \r\n and bare \n */
const char *nl = NULL;
size_t nl_skip = 2; /* bytes to skip past the line ending */
for (const char *q = p; q < end; q++) {
if (*q == '\r' && q + 1 < end && *(q+1) == '\n') {
nl = q; nl_skip = 2; break;
}
if (*q == '\n') {
nl = q; nl_skip = 1; break;
}
}
if (!nl) return RESP_PARSE_MORE;
/* Split on spaces */
/* Split on spaces/tabs */
const char *s = p;
while (s < nl && cmd->argc < VC_MAX_ARGS) {
while (s < nl && *s == ' ') s++;
while (s < nl && (*s == ' ' || *s == '\t')) s++;
if (s >= nl) break;
const char *e2 = s;
while (e2 < nl && *e2 != ' ') e2++;
while (e2 < nl && *e2 != ' ' && *e2 != '\t') e2++;
size_t alen = (size_t)(e2 - s);
cmd->argv[cmd->argc] = strndup(s, alen);
cmd->argl[cmd->argc] = alen;
cmd->argc++;
s = e2;
}
*consumed = (size_t)(nl - buf) + 2;
*consumed = (size_t)(nl - buf) + nl_skip;
return (cmd->argc > 0) ? RESP_PARSE_OK : RESP_PARSE_ERR;
}

Expand Down
Loading
Loading