1pub mod actions;
6
7use crate::client::llm_budget::call_llm_for_client;
8use crate::llm::actions::client_trait::{Client, ClientActionResult};
9use crate::logging::emit::Log;
10use crate::protocol::{ConnectContext, Event};
11use crate::server::bfd::packet;
12use crate::server::bfd::runner::{self, Input, Link, Note};
13use crate::server::bfd::session::Session;
14use crate::server::bfd::ttl;
15use crate::state::client_handles::{ClientCommand, ClientSendOutcome};
16use crate::state::{AccessLogOwner, ClientStatus};
17pub use actions::BfdClientProtocol;
18use anyhow::{Context, Result};
19use serde_json::{json, Value};
20use std::net::{IpAddr, SocketAddr};
21use std::sync::Arc;
22use tokio::sync::mpsc;
23
24pub const MAX_FOLLOWUP_DEPTH: u32 = 8;
26const TURNS: usize = 64;
27
28pub async fn connect(ctx: ConnectContext) -> Result<SocketAddr> {
29 let params = ctx.startup_params.as_ref();
30 let multihop_param = params
31 .map(|p| p.get_optional_bool("multihop"))
32 .transpose()?
33 .flatten();
34 let remote = parse_remote(&ctx.remote_addr, multihop_param).await?;
35 let multihop = multihop_param.unwrap_or(remote.port() == packet::MULTIHOP_PORT);
36 let local_ip = match params
37 .map(|p| p.get_optional_string("local_address"))
38 .transpose()?
39 .flatten()
40 {
41 Some(raw) => raw
42 .parse::<IpAddr>()
43 .with_context(|| format!("local_address {raw:?} is not an IP address"))?,
44 None => route_source(remote).await?,
45 };
46 let timers = crate::server::bfd::actions::timers_from(params)?;
47 let auth = crate::server::bfd::actions::auth_from(params)?;
48
49 let rx = crate::server::socket_helpers::create_reusable_udp_socket(SocketAddr::new(
52 local_ip,
53 remote.port(),
54 ))
55 .await
56 .with_context(|| format!("cannot listen on {local_ip}:{}", remote.port()))?;
57 ttl::enable(&rx).context("asking for the TTL of received packets")?;
58 let tx = Arc::new(runner::tx_socket(local_ip).await?);
59 let local = tx.local_addr()?;
60 let discr = loop {
61 let d: u32 = rand::random();
62 if d != 0 {
63 break d;
64 }
65 };
66 ctx.state
67 .update_client_status(ctx.client_id, ClientStatus::Connected)
68 .await;
69 Log::new(Some(&ctx.status_tx)).info(format!(
70 "BFD client {} session {discr} with {remote} from {local} ({})",
71 ctx.client_id,
72 if multihop { "multihop" } else { "single-hop" }
73 ));
74 let external =
75 crate::client::command_support::register_command_channel(&ctx.state, ctx.client_id).await;
76
77 let (inputs, inputs_rx) = mpsc::channel(64);
78 let (notes_tx, notes) = mpsc::channel(64);
79 let link = Link {
80 socket: tx,
81 dest: remote,
82 label: format!("BFD client {} session {discr}", ctx.client_id),
83 };
84 let session = Session::new(discr, timers, auth, false);
85 let run = tokio::spawn(runner::run(session, link, inputs_rx, notes_tx, None));
86 let run_abort = run.abort_handle();
87 ctx.state.register_client_task(ctx.client_id, run).await;
88
89 let reader_inputs = inputs.clone();
90 let reader = tokio::spawn(async move {
91 let mut buf = [0u8; 2048];
92 while let Ok((n, from, ttl)) = ttl::recv(&rx, &mut buf).await {
93 if from.ip() != remote.ip() {
94 continue;
95 }
96 if !multihop && ttl.is_some_and(|t| t != 255) {
97 tracing::warn!(
98 "BFD client dropped a packet from {from} with TTL {ttl:?} decision=dropped_ttl"
99 );
100 continue;
101 }
102 let packet = match packet::decode(&buf[..n]) {
103 Ok(p) => p,
104 Err(e) => {
105 tracing::warn!("BFD client dropped a datagram from {from}: {e:#}");
106 continue;
107 }
108 };
109 if packet.your_discriminator != 0 && packet.your_discriminator != discr {
110 continue;
111 }
112 let input = Input::Packet {
113 bytes: buf[..n].to_vec(),
114 packet,
115 };
116 if reader_inputs.send(input).await.is_err() {
117 return;
118 }
119 }
120 });
121 let reader_abort = reader.abort_handle();
122 ctx.state.register_client_task(ctx.client_id, reader).await;
123
124 let (turn_tx, turns) = mpsc::channel::<(Event, u32)>(TURNS);
125 let (answers_tx, answers) = mpsc::channel::<(Value, u32)>(TURNS);
126 let _ = turn_tx.try_send((
127 Event::new(
128 &actions::STARTED_EVENT,
129 json!({"peer": remote.to_string(), "local": SocketAddr::new(local_ip, remote.port()).to_string(),
130 "local_discriminator": discr, "multihop": multihop}),
131 ),
132 0,
133 ));
134 let dispatcher = tokio::spawn(run_turns(ctx.clone(), turns, answers_tx));
135 let dispatcher_abort = dispatcher.abort_handle();
136 ctx.state
137 .register_client_task(ctx.client_id, dispatcher)
138 .await;
139
140 let control_ctx = ctx.clone();
141 let peer = remote.ip().to_string();
142 let control = tokio::spawn(async move {
143 control(
144 &control_ctx,
145 &peer,
146 &inputs,
147 notes,
148 turn_tx,
149 answers,
150 external,
151 )
152 .await;
153 let (reply_tx, reply) = tokio::sync::oneshot::channel();
155 let goodbye = Input::Action {
156 action: json!({"type": "bfd_admin_down", "diag": "administratively_down"}),
157 depth: MAX_FOLLOWUP_DEPTH,
158 command: Some(ClientCommand {
159 action: Value::Null,
160 reply_tx,
161 }),
162 };
163 if inputs.send(goodbye).await.is_ok() {
164 let _ = tokio::time::timeout(std::time::Duration::from_secs(1), reply).await;
165 }
166 run_abort.abort();
167 reader_abort.abort();
168 dispatcher_abort.abort();
169 control_ctx
170 .state
171 .update_client_status(control_ctx.client_id, ClientStatus::Disconnected)
172 .await;
173 control_ctx
174 .state
175 .remove_client_handle(control_ctx.client_id)
176 .await;
177 let _ = control_ctx.status_tx.send("__UPDATE_UI__".into());
178 });
179 ctx.state.register_client_task(ctx.client_id, control).await;
180 Ok(local)
181}
182
183async fn parse_remote(raw: &str, multihop: Option<bool>) -> Result<SocketAddr> {
185 let raw = raw.trim();
186 anyhow::ensure!(!raw.is_empty(), "remote_addr is the router's address");
187 if let Ok(ip) = raw.parse::<IpAddr>() {
188 let port = if multihop == Some(true) {
189 packet::MULTIHOP_PORT
190 } else {
191 packet::SINGLE_HOP_PORT
192 };
193 return Ok(SocketAddr::new(ip, port));
194 }
195 tokio::net::lookup_host(raw)
196 .await
197 .with_context(|| format!("cannot resolve {raw}"))?
198 .next()
199 .with_context(|| format!("{raw} resolved to no address"))
200}
201
202async fn route_source(remote: SocketAddr) -> Result<IpAddr> {
204 let probe = tokio::net::UdpSocket::bind(if remote.is_ipv4() {
205 "0.0.0.0:0"
206 } else {
207 "[::]:0"
208 })
209 .await?;
210 probe
211 .connect(remote)
212 .await
213 .with_context(|| format!("no route to {remote}"))?;
214 Ok(probe.local_addr()?.ip())
215}
216
217async fn control(
218 ctx: &ConnectContext,
219 peer: &str,
220 inputs: &mpsc::Sender<Input>,
221 mut notes: mpsc::Receiver<Note>,
222 turns: mpsc::Sender<(Event, u32)>,
223 mut answers: mpsc::Receiver<(Value, u32)>,
224 mut external: mpsc::Receiver<ClientCommand>,
225) {
226 loop {
227 tokio::select! {
228 c = external.recv() => {
229 let Some(command) = c else { return };
230 ctx.state
231 .record_access_log(
232 AccessLogOwner::Client(ctx.client_id.as_u32()),
233 "BFD",
234 None,
235 "injected_action",
236 command.action.clone(),
237 vec![],
238 )
239 .await;
240 match BfdClientProtocol.execute_action(command.action.clone()) {
241 Ok(ClientActionResult::Disconnect) => {
242 crate::client::command_support::reply(command, Ok(ClientSendOutcome::Disconnected));
243 return;
244 }
245 Ok(_) => {
246 let action = command.action.clone();
247 let _ = inputs.send(Input::Action { action, depth: 0, command: Some(command) }).await;
248 }
249 Err(e) => crate::client::command_support::reply(
250 command,
251 Ok(ClientSendOutcome::Rejected { error: e.to_string() }),
252 ),
253 }
254 }
255 a = answers.recv() => {
256 let Some((action, depth)) = a else { return };
257 match BfdClientProtocol.execute_action(action.clone()) {
258 Ok(ClientActionResult::Disconnect) => return,
259 Ok(_) => {
260 let _ = inputs.send(Input::Action { action, depth, command: None }).await;
261 }
262 Err(e) => tracing::warn!("BFD client {} refused {}: {e:#}", ctx.client_id, action["type"]),
263 }
264 }
265 n = notes.recv() => match n {
266 None | Some(Note::Ended { .. }) => return,
267 Some(Note::Changed { previous, snapshot, depth }) => {
268 let data = runner::state_event(peer, previous, &snapshot);
269 Log::new(Some(&ctx.status_tx)).info(format!(
270 "BFD client {}: {} -> {} ({})",
271 ctx.client_id,
272 previous.name(),
273 data["state"].as_str().unwrap_or_default(),
274 data["diag"].as_str().unwrap_or_default()
275 ));
276 if turns.try_send((Event::new(&actions::STATE_EVENT, data), depth)).is_err() {
277 tracing::warn!("BFD client {} dropped a state event: the model is {TURNS} behind decision=turn_queue_full", ctx.client_id);
278 }
279 }
280 },
281 }
282 }
283}
284
285async fn run_turns(
286 ctx: ConnectContext,
287 mut turns: mpsc::Receiver<(Event, u32)>,
288 answers: mpsc::Sender<(Value, u32)>,
289) {
290 let protocol = BfdClientProtocol;
291 while let Some((event, depth)) = turns.recv().await {
292 if depth >= MAX_FOLLOWUP_DEPTH {
293 tracing::warn!("BFD client {} not asking the model about {}: {depth} turns deep decision=followup_depth", ctx.client_id, event.id());
294 continue;
295 }
296 let instruction = ctx
297 .state
298 .get_instruction_for_client(ctx.client_id)
299 .await
300 .unwrap_or_default();
301 let memory = ctx
302 .state
303 .get_memory_for_client(ctx.client_id)
304 .await
305 .unwrap_or_default();
306 match call_llm_for_client(
307 &ctx.llm_client,
308 &ctx.state,
309 ctx.client_id.to_string(),
310 &instruction,
311 &memory,
312 Some(&event),
313 &protocol,
314 &ctx.status_tx,
315 )
316 .await
317 {
318 Ok(result) => {
319 if let Some(memory) = result.memory_updates {
320 ctx.state.set_memory_for_client(ctx.client_id, memory).await;
321 }
322 for action in result.actions {
323 if answers.send((action, depth + 1)).await.is_err() {
324 return;
325 }
326 }
327 }
328 Err(e) => Log::new(Some(&ctx.status_tx)).warn(format!("BFD client handler: {e}")),
329 }
330 }
331}