Skip to main content

netget/server/bolt/
mod.rs

1//! Neo4j's Bolt protocol — the model is the graph database.
2//!
3//! A client opens with the 4-byte magic `60 60 B0 17` and four version proposals; the server
4//! answers with the version it picked (Bolt 5.0–5.8, see [`messages::negotiate`]). From then on
5//! both sides exchange chunked PackStream messages. The model is asked exactly two things — may
6//! this login in ([`actions::BOLT_AUTHENTICATE_EVENT`]), and what does this Cypher query return
7//! ([`actions::BOLT_QUERY_EVENT`]) — and everything else is NetGet's:
8//!
9//! * **The state machine.** CONNECTED → (HELLO) → AUTHENTICATION → (LOGON) → READY; RUN →
10//!   STREAMING; PULL/DISCARD drain it back to READY; BEGIN → TX_READY, RUN → TX_STREAMING,
11//!   COMMIT/ROLLBACK → READY; a FAILURE → FAILED, where every message but RESET and GOODBYE is
12//!   answered IGNORED; RESET → READY from anywhere. A message that arrives ahead of a RESET the
13//!   client already sent is IGNORED (the INTERRUPTED state), and a message the current state does
14//!   not allow is a `Neo.ClientError.Request.Invalid` FAILURE and a close. Bolt 5.0 carries the
15//!   credentials in HELLO and goes straight to READY.
16//! * **Result streaming.** The model answers RUN with every row at once; NetGet sends the column
17//!   list in RUN's SUCCESS, then as many RECORDs as each PULL's `n` asks for with `has_more`, and
18//!   the summary (`type`, `db`, `stats`, a bookmark for writes) at the end. Inside a transaction
19//!   several results may be open at once and PULL/DISCARD address them by `qid`.
20//! * **What costs no model call.** HELLO, ROUTE (a single-server routing table naming the address
21//!   the client dialled), BEGIN/COMMIT/ROLLBACK, RESET, LOGOFF, TELEMETRY, and the admin queries
22//!   `cypher-shell` runs on every connect (`CALL db.ping()`, `CALL
23//!   dbms.licenseAgreementDetails()`, `CALL dbms.components()`), so connecting costs nothing.
24//! * **The password.** With a `password` startup parameter NetGet compares the credential itself,
25//!   in constant time, and a mismatch never reaches the model. The credential never appears in an
26//!   event either way.
27//!
28//! Three bounds, each refused before anything is allocated for it: the handshake must arrive
29//! within `first_byte_timeout_secs`, a message is capped at [`packstream::MAX_MESSAGE_BYTES`]
30//! summed over its chunks, and PackStream nesting at [`packstream::MAX_PACKSTREAM_DEPTH`]. The
31//! read deadlines wrap the read, never the answer, so a query parked for a human under a
32//! `manual` rule is not closed by `idle_timeout_secs`.
33//!
34//! On a backend failure, an unusable answer or no answer, the client gets FAILURE
35//! `Neo.TransientError.General.DatabaseUnavailable` with a fixed message (never the error text)
36//! and the connection stays usable after RESET — Bolt's own recovery path. A refused login closes
37//! the connection, as Neo4j does.
38pub mod actions;
39pub mod messages;
40pub mod packstream;
41pub mod values;
42
43use crate::llm::action_helper::call_llm;
44use crate::llm::actions::protocol_trait::ActionResult;
45use crate::llm::ollama_client::OllamaClient;
46use crate::logging::emit::Log;
47use crate::protocol::Event;
48use crate::server::connection::ConnectionId;
49use crate::state::app_state::AppState;
50use anyhow::Result;
51use messages::Request;
52use packstream::{Dechunker, Value};
53use std::collections::VecDeque;
54use std::net::SocketAddr;
55use std::sync::Arc;
56use std::time::Duration;
57use tokio::io::{AsyncReadExt, AsyncWriteExt};
58use tokio::net::TcpListener;
59use tokio::sync::{mpsc, Mutex};
60
61pub use packstream::{MAX_MESSAGE_BYTES, MAX_PACKSTREAM_DEPTH};
62
63/// Neo4j version the server claims unless `neo4j_version` says otherwise.
64pub const DEFAULT_NEO4J_VERSION: &str = "5.26.0";
65
66/// How long a new connection may take to send its 20-byte handshake. Bolt is client-first and
67/// every driver sends it with the connect, so 30 seconds only ever matters for a peer that is not
68/// speaking Bolt at all.
69pub const FIRST_BYTE_TIMEOUT: Duration = Duration::from_secs(30);
70
71/// How long the server waits for the next message after the handshake. Drivers pool connections
72/// and leave them idle between queries; five minutes keeps a pooled connection useful without
73/// letting abandoned ones accumulate.
74pub const IDLE_TIMEOUT: Duration = Duration::from_secs(300);
75
76/// Concurrent connections admitted before new ones are refused — the house default. Each holds a
77/// task, a message buffer of up to [`MAX_MESSAGE_BYTES`] and the rows of its open results.
78const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
79
80/// Bolt has nothing to say before the handshake, so a peer over the cap is simply closed, as a
81/// Neo4j server at its own `server.bolt.thread_pool_max_size` does.
82const CONNECTION_CAP_REFUSAL: &[u8] = b"";
83
84const REQUEST_INVALID: &str = "Neo.ClientError.Request.Invalid";
85const DATABASE_UNAVAILABLE: &str = "Neo.TransientError.General.DatabaseUnavailable";
86/// Fixed FAILURE messages for the two `WireFailure` categories. Nothing derived from the error
87/// reaches the client.
88const UNAVAILABLE_MESSAGE: &str = "The database is unavailable: the query could not be answered.";
89const OVERLOADED_MESSAGE: &str = "The database is at capacity; retry the query later.";
90const UNAUTHORIZED_MESSAGE: &str = "The client is unauthorized due to authentication failure.";
91
92/// Startup configuration shared by every connection.
93#[derive(Debug, Clone)]
94pub struct BoltConfig {
95    pub password: Option<String>,
96    pub neo4j_version: String,
97    pub first_byte_timeout: Duration,
98    pub idle_timeout: Duration,
99}
100
101pub struct BoltServer;
102
103impl BoltServer {
104    pub async fn spawn_with_llm_actions(
105        listen_addr: SocketAddr,
106        llm_client: OllamaClient,
107        app_state: Arc<AppState>,
108        status_tx: mpsc::UnboundedSender<String>,
109        server_id: crate::state::ServerId,
110        config: BoltConfig,
111    ) -> Result<SocketAddr> {
112        let listener = TcpListener::bind(listen_addr).await?;
113        let local_addr = listener.local_addr()?;
114        Log::new(Some(&status_tx)).info(format!(
115            "Bolt server listening on {} (Neo4j/{})",
116            local_addr, config.neo4j_version
117        ));
118
119        let config = Arc::new(config);
120        let protocol = Arc::new(actions::BoltProtocol::new());
121        let task_registrar = app_state.clone();
122        let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
123        let accept_handle = tokio::spawn(async move {
124            loop {
125                match crate::server::accept_bounded::accept_bounded(
126                    &listener,
127                    &limiter,
128                    CONNECTION_CAP_REFUSAL,
129                    "Bolt",
130                    Some(&status_tx),
131                )
132                .await
133                {
134                    Ok((socket, peer_addr, permit)) => {
135                        let connection_id =
136                            ConnectionId::new(app_state.get_next_unified_id().await);
137                        use crate::state::server::{
138                            ConnectionState as ServerConnectionState, ConnectionStatus,
139                            ProtocolConnectionInfo,
140                        };
141                        let now = crate::utils::clock::Instant::now();
142                        app_state
143                            .add_connection_to_server(
144                                server_id,
145                                ServerConnectionState {
146                                    id: connection_id,
147                                    remote_addr: peer_addr,
148                                    local_addr,
149                                    bytes_sent: 0,
150                                    bytes_received: 0,
151                                    packets_sent: 0,
152                                    packets_received: 0,
153                                    last_activity: now,
154                                    status: ConnectionStatus::Active,
155                                    status_changed_at: now,
156                                    protocol_info: ProtocolConnectionInfo::empty(),
157                                },
158                            )
159                            .await;
160                        let _ = status_tx.send("__UPDATE_UI__".to_string());
161                        Log::new(Some(&status_tx))
162                            .info(format!("Bolt client connected from {}", peer_addr));
163
164                        let session = Session {
165                            peer_addr,
166                            local_addr,
167                            llm_client: llm_client.clone(),
168                            app_state: app_state.clone(),
169                            status_tx: status_tx.clone(),
170                            server_id,
171                            protocol: protocol.clone(),
172                            connection_id,
173                            config: config.clone(),
174                        };
175                        // Tracked, not detached: stop_server must abort this task too.
176                        let task_owner = app_state.clone();
177                        task_owner
178                            .spawn_server_task(server_id, async move {
179                                // Released when the connection ends, so MAX_CONNECTIONS caps
180                                // live connections.
181                                let _permit = permit;
182                                session.run(socket).await
183                            })
184                            .await;
185                    }
186                    Err(e) => {
187                        Log::new(Some(&status_tx)).error(format!("Bolt accept error: {}", e));
188                        break;
189                    }
190                }
191            }
192        });
193
194        task_registrar
195            .register_server_task(server_id, accept_handle)
196            .await;
197        Ok(local_addr)
198    }
199}
200
201/// Where the connection is in Bolt's server state machine.
202#[derive(Debug, Clone, Copy, PartialEq, Eq)]
203enum Phase {
204    /// Handshake done, waiting for HELLO.
205    Connected,
206    /// HELLO answered, waiting for LOGON (Bolt 5.1+).
207    Authentication,
208    Ready,
209    /// An auto-commit result is open.
210    Streaming,
211    TxReady,
212    /// At least one result inside the transaction is open.
213    TxStreaming,
214    Failed,
215}
216
217impl Phase {
218    fn wire_name(self) -> &'static str {
219        match self {
220            Phase::Connected => "CONNECTED",
221            Phase::Authentication => "AUTHENTICATION",
222            Phase::Ready => "READY",
223            Phase::Streaming => "STREAMING",
224            Phase::TxReady => "TX_READY",
225            Phase::TxStreaming => "TX_STREAMING",
226            Phase::Failed => "FAILED",
227        }
228    }
229}
230
231/// One RUN's answer, held until PULL or DISCARD has consumed it.
232struct OpenResult {
233    qid: i64,
234    records: VecDeque<Vec<Value>>,
235    query_type: &'static str,
236    stats: Option<Value>,
237    db: String,
238}
239
240struct Transaction {
241    db: Option<String>,
242    read: bool,
243}
244
245/// Per-connection protocol state.
246struct Conn {
247    minor: u8,
248    phase: Phase,
249    authenticated: bool,
250    user_agent: String,
251    /// The `address` from HELLO's routing context: what the client dialled, and so what a
252    /// routing table must name for the client to come back here.
253    routing_address: Option<String>,
254    tx: Option<Transaction>,
255    results: Vec<OpenResult>,
256    next_qid: i64,
257}
258
259impl Conn {
260    fn new(minor: u8) -> Self {
261        Self {
262            minor,
263            phase: Phase::Connected,
264            authenticated: false,
265            user_agent: String::new(),
266            routing_address: None,
267            tx: None,
268            results: Vec::new(),
269            next_qid: 0,
270        }
271    }
272}
273
274/// What handling one message decided about the connection.
275enum Flow {
276    Continue,
277    Close,
278}
279
280/// Outcome of asking whoever answers queries.
281enum QueryOutcome {
282    Answer(values::QueryAnswer),
283    /// A FAILURE has been queued and the connection is in FAILED.
284    Failed,
285    Close,
286}
287
288/// Outcome of asking whoever answers logins.
289enum Login {
290    Accepted,
291    /// A FAILURE has been queued; close.
292    Refused,
293    Closed,
294}
295
296struct Session {
297    peer_addr: SocketAddr,
298    local_addr: SocketAddr,
299    llm_client: OllamaClient,
300    app_state: Arc<AppState>,
301    status_tx: mpsc::UnboundedSender<String>,
302    server_id: crate::state::ServerId,
303    protocol: Arc<actions::BoltProtocol>,
304    connection_id: ConnectionId,
305    config: Arc<BoltConfig>,
306}
307
308/// Bytes queued for one write, so a PULL's records and summary leave in one `write_all`.
309#[derive(Default)]
310struct Out {
311    bytes: Vec<u8>,
312    messages: u64,
313}
314
315impl Out {
316    fn push(&mut self, message: &Value) {
317        self.bytes.extend(packstream::message_bytes(message));
318        self.messages += 1;
319    }
320}
321
322fn is_reset(raw: &[u8]) -> bool {
323    raw.len() >= 2 && raw[0] == 0xB0 && raw[1] == messages::RESET
324}
325
326impl Session {
327    async fn run(self, socket: tokio::net::TcpStream) {
328        let (mut reader, write_half) = tokio::io::split(socket);
329        let write_half = Arc::new(Mutex::new(write_half));
330
331        // The dashboard's [ disconnect ] goes through this handle. Bolt defines no message a
332        // server may send unprompted, so an injected answer action is validated and reported but
333        // writes nothing; `close_connection` half-closes the socket.
334        let peer_rx = crate::server::peer_support::register_peer_channel(
335            &self.app_state,
336            self.server_id,
337            self.connection_id.as_u32(),
338        )
339        .await;
340        crate::server::peer_support::spawn_peer_command_task(
341            peer_rx,
342            self.protocol.clone(),
343            self.app_state.clone(),
344            self.server_id,
345            self.connection_id.as_u32(),
346            write_half.clone(),
347            self.status_tx.clone(),
348        );
349
350        self.session(&mut reader, &write_half).await;
351
352        self.app_state
353            .remove_peer_handle(self.server_id, self.connection_id.as_u32())
354            .await;
355        let _ = write_half.lock().await.shutdown().await;
356        linger(&mut reader).await;
357        self.app_state
358            .update_connection_status(
359                self.server_id,
360                self.connection_id,
361                crate::state::server::ConnectionStatus::Closed,
362            )
363            .await;
364        let _ = self.status_tx.send("__UPDATE_UI__".to_string());
365    }
366
367    fn log(&self) -> Log<'_> {
368        Log::new(Some(&self.status_tx))
369    }
370
371    async fn write<W>(&self, write_half: &Arc<Mutex<W>>, data: &[u8], messages: u64) -> bool
372    where
373        W: tokio::io::AsyncWrite + Unpin,
374    {
375        if data.is_empty() {
376            return true;
377        }
378        let ok = {
379            let mut writer = write_half.lock().await;
380            writer.write_all(data).await.is_ok() && writer.flush().await.is_ok()
381        };
382        self.app_state
383            .update_connection_stats(
384                self.server_id,
385                self.connection_id,
386                None,
387                Some(data.len() as u64),
388                None,
389                Some(messages.max(1)),
390            )
391            .await;
392        ok
393    }
394
395    async fn received(&self, bytes: usize) {
396        self.app_state
397            .update_connection_stats(
398                self.server_id,
399                self.connection_id,
400                Some(bytes as u64),
401                None,
402                Some(1),
403                None,
404            )
405            .await;
406    }
407
408    async fn session<R, W>(&self, reader: &mut R, write_half: &Arc<Mutex<W>>)
409    where
410        R: tokio::io::AsyncRead + Unpin,
411        W: tokio::io::AsyncWrite + Unpin,
412    {
413        let Some(minor) = self.handshake(reader, write_half).await else {
414            return;
415        };
416        let mut conn = Conn::new(minor);
417        let mut dechunker = Dechunker::new(MAX_MESSAGE_BYTES);
418        let mut queue: VecDeque<Vec<u8>> = VecDeque::new();
419        let mut buf = vec![0u8; 16 * 1024];
420
421        loop {
422            loop {
423                match dechunker.next_message() {
424                    Ok(Some(message)) => queue.push_back(message),
425                    Ok(None) => break,
426                    Err(packstream::FrameError::TooLarge { limit }) => {
427                        self.log().warn(format!(
428                            "Bolt message from {} exceeds {} bytes \
429                             decision=fail_closed_message_too_large",
430                            self.peer_addr, limit
431                        ));
432                        let mut out = Out::default();
433                        out.push(&messages::failure(
434                            conn.minor,
435                            REQUEST_INVALID,
436                            &format!("Message exceeds the {limit}-byte limit of this server."),
437                        ));
438                        self.write(write_half, &out.bytes, out.messages).await;
439                        return;
440                    }
441                }
442            }
443
444            let Some(raw) = queue.pop_front() else {
445                let read =
446                    tokio::time::timeout(self.config.idle_timeout, reader.read(&mut buf)).await;
447                match read {
448                    Err(_) => {
449                        self.log().info(format!(
450                            "Bolt client {} sent nothing for {}s; closing",
451                            self.peer_addr,
452                            self.config.idle_timeout.as_secs()
453                        ));
454                        return;
455                    }
456                    Ok(Ok(0)) => {
457                        self.log()
458                            .info(format!("Bolt client {} disconnected", self.peer_addr));
459                        return;
460                    }
461                    Ok(Ok(n)) => {
462                        self.received(n).await;
463                        dechunker.push(&buf[..n]);
464                    }
465                    Ok(Err(e)) => {
466                        self.log()
467                            .error(format!("Bolt read error from {}: {}", self.peer_addr, e));
468                        return;
469                    }
470                }
471                continue;
472            };
473
474            let interrupted = queue.iter().any(|m| is_reset(m));
475            let mut out = Out::default();
476            let flow = self.handle(&raw, interrupted, &mut conn, &mut out).await;
477            if !self.write(write_half, &out.bytes, out.messages).await {
478                return;
479            }
480            if matches!(flow, Flow::Close) {
481                return;
482            }
483        }
484    }
485
486    /// Read the 20-byte handshake and answer it. `None` means the connection is over.
487    async fn handshake<R, W>(&self, reader: &mut R, write_half: &Arc<Mutex<W>>) -> Option<u8>
488    where
489        R: tokio::io::AsyncRead + Unpin,
490        W: tokio::io::AsyncWrite + Unpin,
491    {
492        let mut hs = [0u8; 20];
493        match tokio::time::timeout(self.config.first_byte_timeout, reader.read_exact(&mut hs)).await
494        {
495            Err(_) => {
496                self.log().info(format!(
497                    "Bolt client {} sent no handshake within {}s; closing",
498                    self.peer_addr,
499                    self.config.first_byte_timeout.as_secs()
500                ));
501                return None;
502            }
503            Ok(Err(_)) => {
504                self.log().info(format!(
505                    "Bolt client {} closed before completing the handshake",
506                    self.peer_addr
507                ));
508                return None;
509            }
510            Ok(Ok(_)) => {}
511        }
512        self.received(hs.len()).await;
513        if hs[..4] != messages::MAGIC {
514            self.log().warn(format!(
515                "Bolt client {} did not open with the Bolt magic (got {:02X?}) \
516                 decision=fail_closed_bad_magic",
517                self.peer_addr,
518                &hs[..4]
519            ));
520            return None;
521        }
522        let mut proposals = [0u8; 16];
523        proposals.copy_from_slice(&hs[4..]);
524        match messages::negotiate(&proposals) {
525            Some((major, minor)) => {
526                self.log().debug(format!(
527                    "Bolt client {} proposed {:02X?}; speaking {}.{}",
528                    self.peer_addr, proposals, major, minor
529                ));
530                if !self.write(write_half, &[0, 0, minor, major], 1).await {
531                    return None;
532                }
533                Some(minor)
534            }
535            None => {
536                self.log().warn(format!(
537                    "Bolt client {} proposed no version in 5.{}..=5.{} ({:02X?}) \
538                     decision=fail_closed_no_common_version",
539                    self.peer_addr,
540                    messages::MIN_MINOR,
541                    messages::MAX_MINOR,
542                    proposals
543                ));
544                self.write(write_half, &[0, 0, 0, 0], 1).await;
545                None
546            }
547        }
548    }
549
550    /// A message the protocol does not allow here: FAILURE `Request.Invalid`, then close.
551    fn protocol_violation(&self, conn: &Conn, out: &mut Out, what: String) -> Flow {
552        self.log().warn(format!(
553            "Bolt client {}: {} decision=fail_closed_protocol_violation",
554            self.peer_addr, what
555        ));
556        out.push(&messages::failure(conn.minor, REQUEST_INVALID, &what));
557        Flow::Close
558    }
559
560    async fn handle(&self, raw: &[u8], interrupted: bool, conn: &mut Conn, out: &mut Out) -> Flow {
561        let value = match packstream::decode(raw) {
562            Ok(v) => v,
563            Err(e) => {
564                let decision = if e == packstream::DecodeError::TooDeep {
565                    "fail_closed_too_deep"
566                } else {
567                    "fail_closed_malformed_message"
568                };
569                self.log().warn(format!(
570                    "Bolt message from {} is not PackStream: {} decision={}",
571                    self.peer_addr, e, decision
572                ));
573                out.push(&messages::failure(
574                    conn.minor,
575                    REQUEST_INVALID,
576                    &format!("Invalid message: {e}"),
577                ));
578                return Flow::Close;
579            }
580        };
581        let request = match messages::parse_request(value) {
582            Ok(r) => r,
583            Err(e) => return self.protocol_violation(conn, out, format!("Invalid message: {e}")),
584        };
585        self.log().trace(format!(
586            "Bolt {} from {} in {}",
587            request.name(),
588            self.peer_addr,
589            conn.phase.wire_name()
590        ));
591
592        match (&request, conn.phase) {
593            (Request::Goodbye, _) => {
594                self.log()
595                    .debug(format!("Bolt client {} said GOODBYE", self.peer_addr));
596                return Flow::Close;
597            }
598            (Request::Reset, Phase::Connected) => {}
599            (Request::Reset, _) => {
600                conn.tx = None;
601                conn.results.clear();
602                conn.phase = if conn.authenticated {
603                    Phase::Ready
604                } else {
605                    Phase::Authentication
606                };
607                out.push(&messages::success(Vec::new()));
608                return Flow::Continue;
609            }
610            // INTERRUPTED: the client has already sent a RESET behind this message.
611            (_, _) if interrupted => {
612                out.push(&messages::ignored());
613                return Flow::Continue;
614            }
615            (_, Phase::Failed) => {
616                out.push(&messages::ignored());
617                return Flow::Continue;
618            }
619            _ => {}
620        }
621
622        match (request, conn.phase) {
623            (Request::Hello { extra }, Phase::Connected) => self.hello(extra, conn, out).await,
624            (Request::Logon { auth }, Phase::Authentication) if conn.minor >= 1 => {
625                match self.authenticate(&auth, conn, out).await {
626                    Login::Accepted => {
627                        conn.authenticated = true;
628                        conn.phase = Phase::Ready;
629                        out.push(&messages::success(Vec::new()));
630                        Flow::Continue
631                    }
632                    Login::Refused | Login::Closed => Flow::Close,
633                }
634            }
635            (Request::Logoff, Phase::Ready) if conn.minor >= 1 => {
636                conn.authenticated = false;
637                conn.phase = Phase::Authentication;
638                out.push(&messages::success(Vec::new()));
639                Flow::Continue
640            }
641            (
642                Request::Run {
643                    query,
644                    params,
645                    extra,
646                },
647                Phase::Ready,
648            ) => self.run_query(&query, &params, &extra, conn, out).await,
649            (
650                Request::Run {
651                    query,
652                    params,
653                    extra,
654                },
655                Phase::TxReady | Phase::TxStreaming,
656            ) => self.run_query(&query, &params, &extra, conn, out).await,
657            (Request::Pull { n, qid }, Phase::Streaming | Phase::TxStreaming) => {
658                self.stream(n, qid, false, conn, out)
659            }
660            (Request::Discard { n, qid }, Phase::Streaming | Phase::TxStreaming) => {
661                self.stream(n, qid, true, conn, out)
662            }
663            (Request::Begin { extra }, Phase::Ready) => {
664                conn.tx = Some(Transaction {
665                    db: extra.get("db").and_then(Value::as_str).map(str::to_string),
666                    read: extra.get("mode").and_then(Value::as_str) == Some("r"),
667                });
668                conn.phase = Phase::TxReady;
669                out.push(&messages::success(Vec::new()));
670                Flow::Continue
671            }
672            (Request::Commit, Phase::TxReady) => {
673                conn.tx = None;
674                conn.phase = Phase::Ready;
675                out.push(&messages::success(vec![(
676                    "bookmark",
677                    Value::String(self.bookmark()),
678                )]));
679                Flow::Continue
680            }
681            (Request::Rollback, Phase::TxReady) => {
682                conn.tx = None;
683                conn.phase = Phase::Ready;
684                out.push(&messages::success(Vec::new()));
685                Flow::Continue
686            }
687            (Request::Route { routing, extra }, Phase::Ready) => {
688                out.push(&self.routing_table(&routing, &extra, conn));
689                Flow::Continue
690            }
691            (Request::Telemetry, Phase::Ready) if conn.minor >= 4 => {
692                out.push(&messages::success(Vec::new()));
693                Flow::Continue
694            }
695            (request, phase) => self.protocol_violation(
696                conn,
697                out,
698                format!(
699                    "Message '{}' cannot be handled by a session in the {} state.",
700                    request.name(),
701                    phase.wire_name()
702                ),
703            ),
704        }
705    }
706
707    fn server_agent(&self) -> String {
708        format!("Neo4j/{}", self.config.neo4j_version)
709    }
710
711    fn bookmark(&self) -> String {
712        format!(
713            "FB:netget:{}:{}",
714            self.server_id.as_u32(),
715            crate::utils::clock::SystemTime::now()
716                .duration_since(crate::utils::clock::UNIX_EPOCH)
717                .map(|d| d.as_millis())
718                .unwrap_or(0)
719        )
720    }
721
722    async fn hello(&self, extra: Value, conn: &mut Conn, out: &mut Out) -> Flow {
723        conn.user_agent = extra
724            .get("user_agent")
725            .and_then(Value::as_str)
726            .unwrap_or("")
727            .to_string();
728        conn.routing_address = extra
729            .get("routing")
730            .and_then(|r| r.get("address"))
731            .and_then(Value::as_str)
732            .map(str::to_string);
733        self.log().info(format!(
734            "Bolt 5.{} HELLO from {} ({})",
735            conn.minor, self.peer_addr, conn.user_agent
736        ));
737        let success = messages::success(vec![
738            ("server", Value::String(self.server_agent())),
739            (
740                "connection_id",
741                Value::String(format!("bolt-{}", self.connection_id.as_u32())),
742            ),
743            ("hints", Value::Map(Vec::new())),
744        ]);
745        if conn.minor == 0 {
746            // Bolt 5.0: the credentials ride in HELLO itself.
747            match self.authenticate(&extra, conn, out).await {
748                Login::Accepted => {
749                    conn.authenticated = true;
750                    conn.phase = Phase::Ready;
751                    out.push(&success);
752                    Flow::Continue
753                }
754                Login::Refused | Login::Closed => Flow::Close,
755            }
756        } else {
757            conn.phase = Phase::Authentication;
758            out.push(&success);
759            Flow::Continue
760        }
761    }
762
763    /// Decide a login. NetGet refuses a configured-password mismatch itself; everything else is
764    /// the `bolt_authenticate` event's to answer.
765    async fn authenticate(&self, auth: &Value, conn: &Conn, out: &mut Out) -> Login {
766        let scheme = auth
767            .get("scheme")
768            .and_then(Value::as_str)
769            .unwrap_or("none")
770            .to_string();
771        let principal = auth
772            .get("principal")
773            .and_then(Value::as_str)
774            .unwrap_or("")
775            .to_string();
776        let credentials = auth.get("credentials").and_then(Value::as_str);
777
778        if let Some(expected) = &self.config.password {
779            let matches = scheme == "basic"
780                && credentials.is_some_and(|c| constant_time_eq(c.as_bytes(), expected.as_bytes()));
781            if !matches {
782                self.log().warn(format!(
783                    "Bolt login {:?} ({}) from {} does not match the configured password \
784                     decision=reject_bad_credentials",
785                    principal, scheme, self.peer_addr
786                ));
787                out.push(&messages::failure(
788                    conn.minor,
789                    actions::UNAUTHORIZED,
790                    UNAUTHORIZED_MESSAGE,
791                ));
792                return Login::Refused;
793            }
794        }
795
796        let event = Event::new(
797            &actions::BOLT_AUTHENTICATE_EVENT,
798            serde_json::json!({
799                "user_agent": conn.user_agent,
800                "scheme": scheme,
801                "principal": principal,
802                "credentials_present": credentials.is_some_and(|c| !c.is_empty()),
803                "password_configured": self.config.password.is_some(),
804                "answer_with": actions::login_answer_with(
805                    &principal,
806                    &scheme,
807                    credentials.is_some_and(|c| !c.is_empty()),
808                ),
809            }),
810        );
811        let (results, refused) = match self.ask(&event, "login").await {
812            Ok(answer) => answer,
813            Err(message) => {
814                out.push(&messages::failure(
815                    conn.minor,
816                    DATABASE_UNAVAILABLE,
817                    message,
818                ));
819                return Login::Refused;
820            }
821        };
822        for result in results {
823            match result {
824                ActionResult::CloseConnection => {
825                    self.log().info(format!(
826                        "Bolt login from {} decision=model_close",
827                        self.peer_addr
828                    ));
829                    return Login::Closed;
830                }
831                ActionResult::Custom { name, data } if name == actions::LOGIN_RESULT => {
832                    if data["accept"].as_bool() == Some(true) {
833                        self.log().info(format!(
834                            "Bolt login {:?} from {} decision=model_answer accepted",
835                            principal, self.peer_addr
836                        ));
837                        return Login::Accepted;
838                    }
839                    let code = data["code"].as_str().unwrap_or(actions::UNAUTHORIZED);
840                    let message = data["message"].as_str().unwrap_or(UNAUTHORIZED_MESSAGE);
841                    self.log().info(format!(
842                        "Bolt login {:?} from {} decision=model_reject code={}",
843                        principal, self.peer_addr, code
844                    ));
845                    out.push(&messages::failure(conn.minor, code, message));
846                    return Login::Refused;
847                }
848                ActionResult::Custom { name, .. } => {
849                    self.log().warn(format!(
850                        "Bolt login from {}: answered with {} decision=fail_closed_mismatched_reply",
851                        self.peer_addr, name
852                    ));
853                    out.push(&messages::failure(
854                        conn.minor,
855                        DATABASE_UNAVAILABLE,
856                        UNAVAILABLE_MESSAGE,
857                    ));
858                    return Login::Refused;
859                }
860                _ => {}
861            }
862        }
863        self.log().warn(format!(
864            "Bolt login from {}: no usable answer decision={}",
865            self.peer_addr,
866            nothing_usable(refused)
867        ));
868        out.push(&messages::failure(
869            conn.minor,
870            DATABASE_UNAVAILABLE,
871            UNAVAILABLE_MESSAGE,
872        ));
873        Login::Refused
874    }
875
876    /// Raise an event and flatten what the model decided, with how many of its actions the
877    /// executor refused. `Err` carries the fixed FAILURE message for the backend-failure
878    /// category; the error itself only reaches the log.
879    async fn ask(
880        &self,
881        event: &Event,
882        what: &str,
883    ) -> Result<(Vec<ActionResult>, usize), &'static str> {
884        match call_llm(
885            &self.llm_client,
886            &self.app_state,
887            self.server_id,
888            Some(self.connection_id),
889            event,
890            self.protocol.as_ref(),
891        )
892        .await
893        {
894            Ok(result) => {
895                for message in &result.messages {
896                    self.log().info(message);
897                }
898                for failure in &result.failures {
899                    self.log()
900                        .warn(format!("Bolt {} answer refused: {:?}", what, failure));
901                }
902                let mut flat = Vec::new();
903                let mut stack = result.protocol_results;
904                stack.reverse();
905                while let Some(item) = stack.pop() {
906                    match item {
907                        ActionResult::Multiple(items) => stack.extend(items.into_iter().rev()),
908                        other => flat.push(other),
909                    }
910                }
911                Ok((flat, result.failures.len()))
912            }
913            Err(e) => {
914                let (category, message) = match crate::utils::WireFailure::classify(&e) {
915                    crate::utils::WireFailure::Overloaded => ("overloaded", OVERLOADED_MESSAGE),
916                    crate::utils::WireFailure::Unavailable => ("unavailable", UNAVAILABLE_MESSAGE),
917                };
918                self.log().warn(format!(
919                    "Bolt {} from {} decision=fail_closed_llm_error category={}",
920                    what, self.peer_addr, category
921                ));
922                self.log().debug(format!("Bolt LLM call failed: {}", e));
923                Err(message)
924            }
925        }
926    }
927
928    async fn run_query(
929        &self,
930        query: &str,
931        params: &Value,
932        extra: &Value,
933        conn: &mut Conn,
934        out: &mut Out,
935    ) -> Flow {
936        let in_tx = conn.tx.is_some();
937        let db = extra
938            .get("db")
939            .and_then(Value::as_str)
940            .map(str::to_string)
941            .or_else(|| conn.tx.as_ref().and_then(|t| t.db.clone()));
942        let read = match &conn.tx {
943            Some(tx) => tx.read,
944            None => extra.get("mode").and_then(Value::as_str) == Some("r"),
945        };
946
947        let answer = if let Some(answer) = self.admin_answer(query) {
948            self.log().debug(format!(
949                "Bolt {:?} from {} answered by NetGet (admin query, no model call)",
950                query, self.peer_addr
951            ));
952            answer
953        } else {
954            let mut data = serde_json::json!({
955                "query": query,
956                "parameters": values::value_to_json(params),
957                "mode": if read { "read" } else { "write" },
958                "in_transaction": in_tx,
959            });
960            if let Some(db) = &db {
961                data["database"] = serde_json::json!(db);
962            }
963            let event = Event::new(&actions::BOLT_QUERY_EVENT, data);
964            let (results, refused) = match self.ask(&event, "query").await {
965                Ok(answer) => answer,
966                Err(message) => {
967                    conn.phase = Phase::Failed;
968                    out.push(&messages::failure(
969                        conn.minor,
970                        DATABASE_UNAVAILABLE,
971                        message,
972                    ));
973                    return Flow::Continue;
974                }
975            };
976            match self.query_outcome(query, results, refused, conn, out) {
977                QueryOutcome::Answer(answer) => answer,
978                QueryOutcome::Failed => return Flow::Continue,
979                QueryOutcome::Close => return Flow::Close,
980            }
981        };
982
983        let qid = conn.next_qid;
984        conn.next_qid += 1;
985        let mut metadata = vec![
986            (
987                "fields",
988                Value::List(answer.fields.iter().cloned().map(Value::String).collect()),
989            ),
990            ("t_first", Value::Int(0)),
991        ];
992        if in_tx {
993            metadata.push(("qid", Value::Int(qid)));
994        }
995        out.push(&messages::success(metadata));
996        conn.results.push(OpenResult {
997            qid,
998            records: answer.records.into(),
999            query_type: answer.query_type,
1000            stats: answer.stats,
1001            db: db.unwrap_or_else(|| "neo4j".to_string()),
1002        });
1003        conn.phase = if in_tx {
1004            Phase::TxStreaming
1005        } else {
1006            Phase::Streaming
1007        };
1008        Flow::Continue
1009    }
1010
1011    /// Turn the model's answer to a query into a result, or queue the FAILURE it chose — or the
1012    /// one NetGet chooses when it chose nothing usable.
1013    fn query_outcome(
1014        &self,
1015        query: &str,
1016        results: Vec<ActionResult>,
1017        refused: usize,
1018        conn: &mut Conn,
1019        out: &mut Out,
1020    ) -> QueryOutcome {
1021        let mut decision = nothing_usable(refused);
1022        for result in results {
1023            match result {
1024                ActionResult::CloseConnection => {
1025                    self.log().info(format!(
1026                        "Bolt query {:?} from {} decision=model_close",
1027                        query, self.peer_addr
1028                    ));
1029                    return QueryOutcome::Close;
1030                }
1031                ActionResult::Custom { name, data } if name == actions::RECORDS_RESULT => {
1032                    match values::query_answer(&data) {
1033                        Ok(answer) => {
1034                            self.log().info(format!(
1035                                "Bolt query {:?} from {} decision=model_answer rows={}",
1036                                query,
1037                                self.peer_addr,
1038                                answer.records.len()
1039                            ));
1040                            return QueryOutcome::Answer(answer);
1041                        }
1042                        Err(e) => {
1043                            self.log().warn(format!(
1044                                "Bolt query from {}: unusable result ({})",
1045                                self.peer_addr, e
1046                            ));
1047                            decision = "fail_closed_invalid_answer";
1048                            break;
1049                        }
1050                    }
1051                }
1052                ActionResult::Custom { name, data } if name == actions::FAILURE_RESULT => {
1053                    let code = data["code"].as_str().unwrap_or(DATABASE_UNAVAILABLE);
1054                    let message = data["message"].as_str().unwrap_or(UNAVAILABLE_MESSAGE);
1055                    self.log().info(format!(
1056                        "Bolt query {:?} from {} decision=model_reject code={}",
1057                        query, self.peer_addr, code
1058                    ));
1059                    conn.phase = Phase::Failed;
1060                    out.push(&messages::failure(conn.minor, code, message));
1061                    return QueryOutcome::Failed;
1062                }
1063                ActionResult::Custom { name, .. } => {
1064                    self.log().warn(format!(
1065                        "Bolt query from {}: answered with {}",
1066                        self.peer_addr, name
1067                    ));
1068                    decision = "fail_closed_mismatched_reply";
1069                    break;
1070                }
1071                _ => {}
1072            }
1073        }
1074        self.log().warn(format!(
1075            "Bolt query {:?} from {}: no usable answer decision={}",
1076            query, self.peer_addr, decision
1077        ));
1078        conn.phase = Phase::Failed;
1079        out.push(&messages::failure(
1080            conn.minor,
1081            DATABASE_UNAVAILABLE,
1082            UNAVAILABLE_MESSAGE,
1083        ));
1084        QueryOutcome::Failed
1085    }
1086
1087    /// PULL (or DISCARD) up to `n` records of the result `qid` addresses (`-1`: the last opened).
1088    fn stream(&self, n: i64, qid: i64, discard: bool, conn: &mut Conn, out: &mut Out) -> Flow {
1089        let index = if qid == -1 {
1090            conn.results.len().checked_sub(1)
1091        } else {
1092            conn.results.iter().position(|r| r.qid == qid)
1093        };
1094        let Some(index) = index else {
1095            return self.protocol_violation(conn, out, format!("No open result with qid {qid}."));
1096        };
1097        let result = &mut conn.results[index];
1098        let take = if n == -1 {
1099            result.records.len()
1100        } else {
1101            (n as usize).min(result.records.len())
1102        };
1103        for row in result.records.drain(..take) {
1104            if !discard {
1105                out.push(&messages::record(row));
1106            }
1107        }
1108        if !result.records.is_empty() {
1109            out.push(&messages::success(vec![("has_more", Value::Bool(true))]));
1110            return Flow::Continue;
1111        }
1112
1113        let result = conn.results.remove(index);
1114        let mut summary = vec![
1115            ("type", Value::string(result.query_type)),
1116            ("t_last", Value::Int(0)),
1117            ("db", Value::String(result.db)),
1118        ];
1119        if let Some(stats) = result.stats {
1120            summary.push(("stats", stats));
1121        }
1122        let autocommit = conn.tx.is_none();
1123        if autocommit && result.query_type != "r" {
1124            summary.push(("bookmark", Value::String(self.bookmark())));
1125        }
1126        out.push(&messages::success(summary));
1127        conn.phase = match (autocommit, conn.results.is_empty()) {
1128            (true, _) => Phase::Ready,
1129            (false, true) => Phase::TxReady,
1130            (false, false) => Phase::TxStreaming,
1131        };
1132        Flow::Continue
1133    }
1134
1135    /// ROUTE: one server in every role — this one, at the address the client used.
1136    fn routing_table(&self, routing: &Value, extra: &Value, conn: &Conn) -> Value {
1137        let address = routing
1138            .get("address")
1139            .and_then(Value::as_str)
1140            .map(str::to_string)
1141            .or_else(|| conn.routing_address.clone())
1142            .unwrap_or_else(|| self.local_addr.to_string());
1143        let db = extra
1144            .get("db")
1145            .and_then(Value::as_str)
1146            .unwrap_or("neo4j")
1147            .to_string();
1148        let server = |role: &str| {
1149            Value::map([
1150                (
1151                    "addresses",
1152                    Value::List(vec![Value::String(address.clone())]),
1153                ),
1154                ("role", Value::string(role)),
1155            ])
1156        };
1157        messages::success(vec![(
1158            "rt",
1159            Value::map([
1160                ("ttl", Value::Int(300)),
1161                ("db", Value::String(db)),
1162                (
1163                    "servers",
1164                    Value::List(vec![server("WRITE"), server("READ"), server("ROUTE")]),
1165                ),
1166            ]),
1167        )])
1168    }
1169
1170    /// The admin queries `cypher-shell` runs on connect, answered here so connecting costs no
1171    /// model call. Matched on the whole query, whitespace- and case-insensitively.
1172    fn admin_answer(&self, query: &str) -> Option<values::QueryAnswer> {
1173        let normalized = query
1174            .trim()
1175            .trim_end_matches(';')
1176            .split_whitespace()
1177            .collect::<Vec<_>>()
1178            .join(" ")
1179            .to_ascii_lowercase();
1180        let version = Value::List(vec![Value::String(self.config.neo4j_version.clone())]);
1181        let (fields, row): (&[&str], Vec<Value>) = match normalized.as_str() {
1182            "call db.ping()" => (&["success"], vec![Value::Bool(true)]),
1183            "call dbms.licenseagreementdetails()" => (
1184                &["status", "daysLeftOnTrial", "totalTrialDays"],
1185                vec![Value::string("yes"), Value::Int(0), Value::Int(0)],
1186            ),
1187            "call dbms.components()" => (
1188                &["name", "versions", "edition"],
1189                vec![
1190                    Value::string("Neo4j Kernel"),
1191                    version,
1192                    Value::string("community"),
1193                ],
1194            ),
1195            "call dbms.components() yield versions" => (&["versions"], vec![version]),
1196            _ => return None,
1197        };
1198        Some(values::QueryAnswer {
1199            fields: fields.iter().map(|f| f.to_string()).collect(),
1200            records: vec![row],
1201            stats: None,
1202            query_type: "r",
1203        })
1204    }
1205}
1206
1207/// The decision token for an answer with nothing usable in it: the executor refused what the
1208/// model sent (a malformed record, an invalid code), or the model sent nothing at all.
1209fn nothing_usable(refused: usize) -> &'static str {
1210    if refused > 0 {
1211        "fail_closed_invalid_answer"
1212    } else {
1213        "model_silent"
1214    }
1215}
1216
1217fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
1218    // Length is not secret-bearing enough to hide here; the contents are compared in full.
1219    if a.len() != b.len() {
1220        return false;
1221    }
1222    a.iter().zip(b).fold(0u8, |acc, (x, y)| acc | (x ^ y)) == 0
1223}
1224
1225/// How long, and how much, the server keeps reading after it has sent its last message and
1226/// half-closed, so a peer with pipelined messages in flight reads the FAILURE and then FIN rather
1227/// than an RST that can destroy it. Bounded both ways.
1228const LINGER_TIME: Duration = Duration::from_secs(2);
1229const LINGER_BYTES: usize = 64 * 1024;
1230
1231async fn linger<R: tokio::io::AsyncRead + Unpin>(reader: &mut R) {
1232    let deadline = tokio::time::Instant::now() + LINGER_TIME;
1233    let mut sink = [0u8; 4096];
1234    let mut drained = 0usize;
1235    while drained < LINGER_BYTES {
1236        match tokio::time::timeout_at(deadline, reader.read(&mut sink)).await {
1237            Ok(Ok(n)) if n > 0 => drained += n,
1238            _ => break,
1239        }
1240    }
1241}