Skip to main content

netget/client/sunrpc/
actions.rs

1//! What the model asks a portmapper/rpcbind: what is registered (dump), where one program is
2//! (getport, getaddr), to register or remove a program (set, unset), and the time.
3use 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
194/// The call one action makes: (portmapper version, procedure, XDR arguments).
195pub 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
276/// A successful call's result body, as the handler reads it.
277pub 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}