1use crate::llm::actions::{
4 client_trait::{Client, ClientActionResult},
5 protocol_trait::Protocol,
6 ActionDefinition, ParameterDefinition, StartupExamples,
7};
8use crate::protocol::{log_template::LogTemplate, ConnectContext, EventType};
9use crate::server::sunrpc::{actions::p, wire};
10use crate::state::app_state::AppState;
11use anyhow::{bail, ensure, Context, Result};
12use serde_json::{json, Value};
13use std::sync::LazyLock;
14
15#[derive(Default)]
16pub struct SunRpcClientProtocol;
17impl SunRpcClientProtocol {
18 pub fn new() -> Self {
19 Self
20 }
21}
22
23fn action(
24 name: &str,
25 description: &str,
26 parameters: Vec<crate::llm::actions::Parameter>,
27 example: Value,
28) -> ActionDefinition {
29 ActionDefinition {
30 name: name.into(),
31 description: description.into(),
32 parameters,
33 example,
34 log_template: Some(LogTemplate::new().with_info(format!(
35 "-> portmapper {}",
36 name.trim_start_matches("sunrpc_")
37 ))),
38 }
39}
40
41fn version_param(default: u32) -> crate::llm::actions::Parameter {
42 p(
43 "version",
44 "number",
45 &format!("Portmapper version to speak: 2 (PMAP), 3 or 4 (RPCBIND); default {default}"),
46 false,
47 )
48}
49
50fn program_params() -> Vec<crate::llm::actions::Parameter> {
51 vec![
52 p(
53 "program",
54 "number",
55 "The RPC program number, e.g. 100003 for NFS",
56 true,
57 ),
58 p(
59 "program_version",
60 "number",
61 "The program's version, e.g. 3",
62 true,
63 ),
64 ]
65}
66
67pub fn actions() -> Vec<ActionDefinition> {
68 let mut getport = program_params();
69 getport.push(p("protocol", "string", "tcp (default) or udp", false));
70 let mut getaddr = program_params();
71 getaddr.push(p(
72 "protocol",
73 "string",
74 "Network id: tcp (default), udp, tcp6 or udp6",
75 false,
76 ));
77 getaddr.push(version_param(4));
78 let mut set = program_params();
79 set.extend([
80 p(
81 "protocol",
82 "string",
83 "tcp (default), udp, tcp6 or udp6 (version 2: tcp or udp)",
84 false,
85 ),
86 p("port", "number", "The port the program listens on", true),
87 version_param(2),
88 ]);
89 let mut unset = program_params();
90 unset.extend([
91 p(
92 "protocol",
93 "string",
94 "tcp or udp (version 2, default tcp); version 3-4: empty for every transport",
95 false,
96 ),
97 version_param(2),
98 ]);
99 vec![
100 action(
101 "sunrpc_null",
102 "Ping the portmapper (procedure 0).",
103 vec![version_param(2)],
104 json!({"type": "sunrpc_null"}),
105 ),
106 action(
107 "sunrpc_dump",
108 "List every registered program.",
109 vec![version_param(4)],
110 json!({"type": "sunrpc_dump", "version": 4}),
111 ),
112 action(
113 "sunrpc_getport",
114 "Ask where a program listens (PMAP v2 GETPORT): a port, 0 if not registered.",
115 getport,
116 json!({"type": "sunrpc_getport", "program": 100003, "program_version": 3, "protocol": "tcp"}),
117 ),
118 action(
119 "sunrpc_getaddr",
120 "Ask a program's universal address (RPCBIND GETADDR): empty if not registered.",
121 getaddr,
122 json!({"type": "sunrpc_getaddr", "program": 100003, "program_version": 3, "protocol": "tcp"}),
123 ),
124 action(
125 "sunrpc_set",
126 "Register a program at a port.",
127 set,
128 json!({"type": "sunrpc_set", "program": 536870913, "program_version": 1, "protocol": "tcp", "port": 4242}),
129 ),
130 action(
131 "sunrpc_unset",
132 "Remove a program's registration.",
133 unset,
134 json!({"type": "sunrpc_unset", "program": 536870913, "program_version": 1, "protocol": "tcp"}),
135 ),
136 action(
137 "sunrpc_gettime",
138 "Ask the server's time (RPCBIND GETTIME).",
139 vec![],
140 json!({"type": "sunrpc_gettime"}),
141 ),
142 action(
143 "disconnect",
144 "Close the connection.",
145 vec![],
146 json!({"type": "disconnect"}),
147 ),
148 ]
149}
150
151pub static CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
152 EventType::new(
153 "sunrpc_connected",
154 "Connected to the portmapper over TCP.",
155 json!({"type": "sunrpc_dump", "version": 4}),
156 )
157 .with_parameters(vec![p(
158 "remote_addr",
159 "string",
160 "The portmapper's address and port",
161 true,
162 )])
163 .with_actions(actions())
164});
165
166pub static REPLY_EVENT: LazyLock<EventType> = LazyLock::new(|| {
167 EventType::new(
168 "sunrpc_reply",
169 "The portmapper answered a call.",
170 json!({"type": "sunrpc_getport", "program": 100003, "program_version": 3}),
171 )
172 .with_parameters(vec![
173 p("operation", "string", "The action that was performed", true),
174 p("ok", "boolean", "Whether the call was accepted and succeeded", true),
175 p("result", "any", "dump: the mappings; getport: the port; getaddr: the address; set/unset: true or false; gettime: unix seconds", true),
176 p("error", "string", "Why the call failed, e.g. PROG_UNAVAIL or PROC_UNAVAIL", false),
177 ])
178 .with_actions(actions())
179});
180
181fn version(v: &Value, default: u32) -> Result<u32> {
182 let n = v["version"].as_u64().unwrap_or(u64::from(default));
183 ensure!((2..=4).contains(&n), "version must be 2, 3 or 4");
184 Ok(n as u32)
185}
186
187fn u32_of(v: &Value, k: &str) -> Result<u32> {
188 let n = v[k]
189 .as_u64()
190 .with_context(|| format!("{k} must be a number"))?;
191 u32::try_from(n).with_context(|| format!("{k} is a 32-bit number"))
192}
193
194pub fn call(v: &Value) -> Result<(u32, u32, Vec<u8>)> {
196 let protocol = v["protocol"].as_str().unwrap_or("tcp");
197 let mut w = wire::Writer::default();
198 Ok(match v["type"].as_str().unwrap_or_default() {
199 "sunrpc_null" => (version(v, 2)?, 0, Vec::new()),
200 "sunrpc_dump" => (version(v, 4)?, 4, Vec::new()),
201 "sunrpc_gettime" => (4, 6, Vec::new()),
202 "sunrpc_getport" => {
203 let prot = wire::protocol_of(protocol)
204 .filter(|_| !protocol.ends_with('6'))
205 .context("getport protocol must be tcp or udp")?;
206 w.u32(u32_of(v, "program")?)
207 .u32(u32_of(v, "program_version")?)
208 .u32(prot)
209 .u32(0);
210 (2, 3, w.0)
211 }
212 "sunrpc_getaddr" => {
213 let ver = version(v, 4)?;
214 ensure!(ver >= 3, "getaddr is RPCBIND: version 3 or 4");
215 wire::write_rpcb(
216 &mut w,
217 &wire::Rpcb {
218 program: u32_of(v, "program")?,
219 version: u32_of(v, "program_version")?,
220 netid: protocol.into(),
221 addr: String::new(),
222 owner: String::new(),
223 },
224 );
225 (ver, 3, w.0)
226 }
227 t @ ("sunrpc_set" | "sunrpc_unset") => {
228 let set = t == "sunrpc_set";
229 let ver = version(v, 2)?;
230 let port = if set {
231 let port = v["port"].as_u64().context("port is required")?;
232 ensure!(port > 0 && port <= 65535, "port must be 1-65535");
233 port as u16
234 } else {
235 0
236 };
237 if ver == 2 {
238 let prot = wire::protocol_of(protocol)
239 .filter(|_| !protocol.ends_with('6'))
240 .context("version 2 registers tcp or udp")?;
241 w.u32(u32_of(v, "program")?)
242 .u32(u32_of(v, "program_version")?)
243 .u32(prot)
244 .u32(u32::from(port));
245 } else {
246 ensure!(
247 protocol.is_empty() || wire::protocol_of(protocol).is_some(),
248 "protocol must be tcp, udp, tcp6 or udp6"
249 );
250 let host = if protocol.ends_with('6') {
251 "::1"
252 } else {
253 "127.0.0.1"
254 };
255 wire::write_rpcb(
256 &mut w,
257 &wire::Rpcb {
258 program: u32_of(v, "program")?,
259 version: u32_of(v, "program_version")?,
260 netid: protocol.into(),
261 addr: if set {
262 wire::uaddr(host.parse()?, port)
263 } else {
264 String::new()
265 },
266 owner: "netget".into(),
267 },
268 );
269 }
270 (ver, if set { 1 } else { 2 }, w.0)
271 }
272 t => bail!("Unknown portmapper client action {t:?}"),
273 })
274}
275
276pub fn result(v: &Value, version: u32, body: &[u8]) -> Result<Value> {
278 let mut r = wire::Reader::new(body);
279 Ok(match v["type"].as_str().unwrap_or_default() {
280 "sunrpc_null" => Value::Null,
281 "sunrpc_dump" if version == 2 => Value::Array(
282 wire::read_pmaplist(body)?
283 .iter()
284 .map(wire::mapping_json)
285 .collect(),
286 ),
287 "sunrpc_dump" => Value::Array(wire::read_rpcblist(body)?),
288 "sunrpc_getport" | "sunrpc_gettime" => json!(r.u32()?),
289 "sunrpc_getaddr" => {
290 let a = r.string()?;
291 let port = wire::parse_uaddr(&a).map(|s| s.port()).ok();
292 json!({"address": a, "port": port})
293 }
294 "sunrpc_set" | "sunrpc_unset" => json!(r.bool()?),
295 _ => Value::Null,
296 })
297}
298
299impl Protocol for SunRpcClientProtocol {
300 fn protocol_name(&self) -> &'static str {
301 "SunRPC"
302 }
303 fn stack_name(&self) -> &'static str {
304 "ETH>IP>TCP>ONC-RPC>Portmapper"
305 }
306 fn keywords(&self) -> Vec<&'static str> {
307 vec!["sunrpc", "portmapper", "rpcbind", "rpcinfo", "onc rpc"]
308 }
309 fn description(&self) -> &'static str {
310 "ONC RPC portmapper/rpcbind client over TCP: dump, getport, getaddr, set, unset, gettime"
311 }
312 fn get_async_actions(&self, _: &AppState) -> Vec<ActionDefinition> {
313 actions()
314 }
315 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
316 vec![]
317 }
318 fn get_event_types(&self) -> Vec<EventType> {
319 vec![CONNECTED_EVENT.clone(), REPLY_EVENT.clone()]
320 }
321 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
322 vec![]
323 }
324 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
325 use crate::protocol::metadata::*;
326 ProtocolMetadataV2::builder()
327 .state(DevelopmentState::Experimental)
328 .privilege_requirement(PrivilegeRequirement::None)
329 .implementation("The server's ONC RPC codec as a caller: one TCP connection with record marking, AUTH_NONE calls to program 100000 (PMAP v2, RPCBIND v3/v4), replies matched by xid")
330 .llm_control("Which programs to look up, register and remove, and what to do with each answer")
331 .e2e_testing("tests/client/sunrpc: the system rpcbind (libtirpc) on 127.0.0.1:111, read back with rpcinfo")
332 .notes("TCP only, AUTH_NONE only. Replies are capped at 256 KiB. A call waits at most 10 s. A handler chain stops after 8 follow-ups.")
333 .build()
334 }
335 fn example_prompt(&self) -> &'static str {
336 "Ask the portmapper at 127.0.0.1:111 where NFS v3 listens over TCP"
337 }
338 fn get_startup_examples(&self) -> StartupExamples {
339 let llm = json!({"type":"open_client","protocol":"sunrpc","remote_addr":"127.0.0.1:111",
340 "instruction":"List every registered program"});
341 let mut static_example = llm.clone();
342 static_example["event_handlers"] = json!([{"event_pattern":"sunrpc_connected","handler":{"type":"static","actions":[{"type":"sunrpc_dump","version":4}]}}]);
343 let mut scripted = llm.clone();
344 scripted["event_handlers"] = json!([{"event_pattern":"*","handler":{"type":"script","language":"python","code":"import json,sys\ni=json.load(sys.stdin)\na=[{'type':'sunrpc_getport','program':100003,'program_version':3}] if i['event_type_id']=='sunrpc_connected' else []\nprint(json.dumps({'actions':a}))"}}]);
345 StartupExamples::new(llm, scripted, static_example)
346 }
347 fn group_name(&self) -> &'static str {
348 "Network Services"
349 }
350}
351
352impl Client for SunRpcClientProtocol {
353 fn connect(
354 &self,
355 ctx: ConnectContext,
356 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<std::net::SocketAddr>> + Send>>
357 {
358 Box::pin(super::connect(ctx))
359 }
360 fn execute_action(&self, v: Value) -> Result<ClientActionResult> {
361 let name = v["type"].as_str().unwrap_or_default().to_string();
362 if name == "disconnect" {
363 return Ok(ClientActionResult::Disconnect);
364 }
365 call(&v)?;
366 Ok(ClientActionResult::Custom { name, data: v })
367 }
368}