1use super::wire;
6use crate::llm::actions::{
7 protocol_trait::{ActionResult, Protocol, Server},
8 ActionDefinition, Parameter, ParameterDefinition, StartupExamples,
9};
10use crate::protocol::{log_template::LogTemplate, EventType, SpawnContext};
11use crate::state::app_state::AppState;
12use anyhow::{bail, ensure, Context, Result};
13use serde_json::{json, Value};
14use std::sync::LazyLock;
15
16pub const MAPPINGS: &str = "sunrpc_mappings";
17pub const ACCEPT: &str = "sunrpc_accept";
18pub const REJECT: &str = "sunrpc_reject";
19
20#[derive(Default, Clone)]
21pub struct SunRpcProtocol;
22
23impl SunRpcProtocol {
24 pub fn new() -> Self {
25 Self
26 }
27}
28
29pub fn p(name: &str, type_hint: &str, description: &str, required: bool) -> Parameter {
30 Parameter {
31 name: name.into(),
32 type_hint: type_hint.into(),
33 description: description.into(),
34 required,
35 }
36}
37
38fn action(
39 name: &str,
40 description: &str,
41 parameters: Vec<Parameter>,
42 example: Value,
43 info: &str,
44) -> ActionDefinition {
45 ActionDefinition {
46 name: name.into(),
47 description: description.into(),
48 parameters,
49 example,
50 log_template: Some(LogTemplate::new().with_info(info)),
51 }
52}
53
54pub fn mappings_action() -> ActionDefinition {
55 action(
56 MAPPINGS,
57 "Answer a lookup or dump with the registered programs. Give every mapping you hold (or just the matching ones): NetGet picks what the procedure returns. An empty list means nothing is registered.",
58 vec![p(
59 "mappings",
60 "array",
61 "Each {program, version, protocol (tcp, udp, tcp6 or udp6), port, owner?}, e.g. NFS 100003 v3 on tcp port 2049",
62 true,
63 )],
64 json!({"type": MAPPINGS, "mappings": [
65 {"program": 100003, "version": 3, "protocol": "tcp", "port": 2049},
66 {"program": 100005, "version": 3, "protocol": "udp", "port": 20048}
67 ]}),
68 "-> portmapper {mappings}",
69 )
70}
71
72pub fn accept_action() -> ActionDefinition {
73 action(
74 ACCEPT,
75 "Accept the registration or removal (the caller is told it succeeded).",
76 vec![],
77 json!({"type": ACCEPT}),
78 "-> portmapper accept",
79 )
80}
81
82pub fn reject_action() -> ActionDefinition {
83 action(
84 REJECT,
85 "Refuse the registration or removal (the caller is told it failed).",
86 vec![],
87 json!({"type": REJECT}),
88 "-> portmapper reject",
89 )
90}
91
92fn common() -> Vec<Parameter> {
93 vec![
94 p(
95 "transport",
96 "string",
97 "tcp or udp: how the call arrived",
98 true,
99 ),
100 p(
101 "rpc_version",
102 "number",
103 "The portmapper version asked: 2 (PMAP), 3 or 4 (RPCBIND)",
104 true,
105 ),
106 ]
107}
108
109pub static QUERY_EVENT: LazyLock<EventType> = LazyLock::new(|| {
110 let mut params = common();
111 params.extend([
112 p(
113 "procedure",
114 "string",
115 "dump, getport, getaddr, getversaddr or getaddrlist",
116 true,
117 ),
118 p(
119 "program",
120 "number",
121 "The program looked up (absent for dump)",
122 false,
123 ),
124 p(
125 "program_name",
126 "string",
127 "Its well-known name, when it has one (nfs, mountd, …)",
128 false,
129 ),
130 p("program_version", "number", "The version looked up", false),
131 p(
132 "protocol",
133 "string",
134 "The transport looked up: tcp, udp, tcp6 or udp6",
135 false,
136 ),
137 ]);
138 EventType::new(
139 "sunrpc_query",
140 "A client asks which programs are registered (dump) or where one program is (getport/getaddr). Answer sunrpc_mappings.",
141 mappings_action().example.clone(),
142 )
143 .with_parameters(params)
144 .with_actions(vec![mappings_action()])
145});
146
147pub static REGISTER_EVENT: LazyLock<EventType> = LazyLock::new(|| {
148 let mut params = common();
149 params.extend([
150 p(
151 "operation",
152 "string",
153 "set (register) or unset (remove)",
154 true,
155 ),
156 p(
157 "program",
158 "number",
159 "The program being registered or removed",
160 true,
161 ),
162 p(
163 "program_name",
164 "string",
165 "Its well-known name, when it has one",
166 false,
167 ),
168 p(
169 "program_version",
170 "number",
171 "The version of the program being registered",
172 true,
173 ),
174 p(
175 "protocol",
176 "string",
177 "tcp, udp, tcp6 or udp6 (empty on an unset: every transport)",
178 true,
179 ),
180 p("port", "number", "The port it listens on (set only)", false),
181 p(
182 "owner",
183 "string",
184 "The owner the caller claims (RPCBIND only)",
185 false,
186 ),
187 p(
188 "credentials",
189 "object",
190 "The call's credentials: {flavor: none} or AUTH_SYS {uid, gid, machine}",
191 true,
192 ),
193 ]);
194 EventType::new(
195 "sunrpc_register",
196 "A program registers itself (set) or is removed (unset). Accept or reject; remember what you accept, since you answer later lookups.",
197 json!({"type": ACCEPT}),
198 )
199 .with_parameters(params)
200 .with_actions(vec![accept_action(), reject_action()])
201});
202
203pub fn check(v: &Value) -> Result<()> {
204 match v["type"].as_str().unwrap_or_default() {
205 MAPPINGS => {
206 let list = v["mappings"]
207 .as_array()
208 .context("mappings must be an array")?;
209 ensure!(
210 list.len() <= wire::MAX_MAPPINGS,
211 "at most {} mappings",
212 wire::MAX_MAPPINGS
213 );
214 for m in list {
215 wire::mapping_from_json(m)?;
216 }
217 Ok(())
218 }
219 ACCEPT | REJECT => Ok(()),
220 other => bail!("Unknown portmapper action {other:?}"),
221 }
222}
223
224impl Protocol for SunRpcProtocol {
225 fn protocol_name(&self) -> &'static str {
226 "SunRPC"
227 }
228 fn stack_name(&self) -> &'static str {
229 "ETH>IP>TCP|UDP>ONC-RPC>Portmapper"
230 }
231 fn keywords(&self) -> Vec<&'static str> {
232 vec![
233 "sunrpc",
234 "onc rpc",
235 "portmapper",
236 "portmap",
237 "rpcbind",
238 "rpcinfo",
239 "111",
240 ]
241 }
242 fn description(&self) -> &'static str {
243 "ONC RPC portmapper/rpcbind (PMAP v2, RPCBIND v3 and v4) on TCP and UDP: tells clients where RPC programs such as NFS and mountd listen"
244 }
245 fn get_async_actions(&self, _: &AppState) -> Vec<ActionDefinition> {
246 Vec::new()
247 }
248 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
249 vec![mappings_action(), accept_action(), reject_action()]
250 }
251 fn get_event_types(&self) -> Vec<EventType> {
252 vec![QUERY_EVENT.clone(), REGISTER_EVENT.clone()]
253 }
254 fn default_binding(&self) -> Option<crate::protocol::BindingDefaults> {
255 Some(crate::protocol::BindingDefaults::port_based("127.0.0.1", 0))
256 }
257 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
258 Vec::new()
259 }
260 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
261 use crate::protocol::metadata::*;
262 ProtocolMetadataV2::builder()
263 .state(DevelopmentState::Experimental)
264 .privilege_requirement(PrivilegeRequirement::PrivilegedPort(111))
265 .well_known_port(111)
266 .implementation("Hand-rolled ONC RPC (src/server/sunrpc/wire.rs): calls and replies with AUTH_NONE/AUTH_SYS, TCP record marking and UDP datagrams on the same port; PMAP v2 NULL/SET/UNSET/GETPORT/DUMP and RPCBIND v3/v4 NULL/SET/UNSET/GETADDR/DUMP/GETTIME/GETVERSADDR/GETADDRLIST; RPC_MISMATCH, PROG_UNAVAIL, PROG_MISMATCH, PROC_UNAVAIL and GARBAGE_ARGS answered in Rust")
267 .llm_control("Every lookup and dump (the mappings it answers with) and every registration and removal (accept or reject)")
268 .e2e_testing("tests/server/sunrpc: the stock libtirpc rpcinfo (-p, -s, -l, -T tcp|udp, -a) in a network namespace whose port 111 is DNATed to NetGet; raw XDR for the error replies and bounds")
269 .notes("Only the portmapper program (100000) is served; calls to any other program are PROG_UNAVAIL. CALLIT/INDIRECT and the statistics procedures are PROC_UNAVAIL. NetGet stores no registrations: the model holds them and answers lookups. Records are capped at 256 KiB in 64 fragments. A model failure answers SYSTEM_ERR.")
270 .answers_on_failure()
271 .request_only("Every reply answers an RPC call")
272 .max_inbound_bytes(wire::MAX_RECORD)
273 .build()
274 }
275 fn example_prompt(&self) -> &'static str {
276 "Portmapper on port 111 that says NFS v3 is on TCP 2049 and mountd v3 on UDP 20048"
277 }
278 fn get_startup_examples(&self) -> StartupExamples {
279 let base = json!({"type":"open_server","base_stack":"sunrpc","port":0,
280 "instruction":"Say NFS 100003 v3 is on tcp 2049 and mountd 100005 v3 on udp 20048; accept every registration and include it afterwards"});
281 let mut static_example = base.clone();
282 static_example["event_handlers"] = json!([
283 {"event_pattern":"sunrpc_query","handler":{"type":"static","actions":[mappings_action().example]}},
284 {"event_pattern":"sunrpc_register","handler":{"type":"static","actions":[{"type":REJECT}]}}
285 ]);
286 let mut scripted = base.clone();
287 scripted["event_handlers"] = json!([{"event_pattern":"*","handler":{"type":"script","language":"python",
288 "code":"import json,sys\ni=json.load(sys.stdin)\nif i['event_type_id']=='sunrpc_register':\n a=[{'type':'sunrpc_accept'}]\nelse:\n a=[{'type':'sunrpc_mappings','mappings':[{'program':100003,'version':3,'protocol':'tcp','port':2049}]}]\nprint(json.dumps({'actions':a}))"}}]);
289 StartupExamples::new(base, scripted, static_example)
290 }
291 fn group_name(&self) -> &'static str {
292 "Network Services"
293 }
294}
295
296impl Server for SunRpcProtocol {
297 fn spawn(
298 &self,
299 ctx: SpawnContext,
300 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<std::net::SocketAddr>> + Send>>
301 {
302 Box::pin(super::spawn(ctx))
303 }
304 fn execute_action(&self, action: Value) -> Result<ActionResult> {
305 check(&action)?;
306 let name = action["type"].as_str().unwrap_or_default().to_string();
307 Ok(ActionResult::Custom { name, data: action })
308 }
309}