clawft_kernel/workload_runtime/
container_relay.rs1use std::net::SocketAddr;
27
28use super::container_cmd::{COG_PATH, CONTAINER_USER};
29use super::host_contract::HostContract;
30use super::types::RuntimeError;
31
32pub const RELAY_PATH: &str = "/weftos-ingest-relay.py";
34pub const RELAY_FILE: &str = "ingest-relay.py";
36pub const COG_INGEST_ADDR: &str = "127.0.0.1:80";
38pub const RELAY_CAPS: [&str; 3] = ["CAP_NET_BIND_SERVICE", "CAP_SETUID", "CAP_SETGID"];
40
41pub fn relay_upstream(
48 host: &HostContract,
49 network: Option<&str>,
50) -> Result<Option<SocketAddr>, RuntimeError> {
51 let Some(up) = host.ingest_upstream else {
52 return Ok(None);
53 };
54 let bad = |m: &str| Err(RuntimeError::InvalidConfig(m.into()));
55 match network {
56 Some("none") => bad("an ingest upstream needs a network; network=none has none"),
57 Some("host") => bad(
58 "with network=host the cog reaches the node's 127.0.0.1:80 directly; \
59 leave ingest_upstream unset",
60 ),
61 _ if up.ip().is_loopback() || up.ip().is_unspecified() || up.port() == 0 => bad(
62 "the ingest upstream must be an address the container can reach \
63 (not loopback or unspecified, non-zero port)",
64 ),
65 _ => Ok(Some(up)),
66 }
67}
68
69pub fn dockerfile(base_image: &str, upstream: SocketAddr) -> String {
71 let (uid, gid) = CONTAINER_USER.split_once(':').unwrap_or(("65534", "65534"));
72 format!(
73 "FROM {base_image}\n\
74 COPY cog {COG_PATH}\n\
75 COPY {RELAY_FILE} {RELAY_PATH}\n\
76 RUN [\"python3\", \"-B\", \"-c\", \"import ctypes, os, socket, threading\"]\n\
77 USER 0:0\n\
78 WORKDIR /\n\
79 ENTRYPOINT [\"python3\", \"-B\", \"-u\", \"{RELAY_PATH}\", \"--listen\", \"{COG_INGEST_ADDR}\", \
80 \"--upstream\", \"{upstream}\", \"--uid\", \"{uid}\", \"--gid\", \"{gid}\", \"--\", \"{COG_PATH}\"]\n"
81 )
82}
83
84pub const RELAY_SCRIPT: &str = r#"import os, signal, socket, sys, threading
86
87MAX_CONNS = 8
88MAX_BYTES = 2 * 1024 * 1024
89IDLE_SECS = 30
90
91
92def log(msg):
93 sys.stderr.write("[ingest-relay] %s\n" % msg)
94
95
96def addr(s):
97 host, _, port = s.rpartition(":")
98 return host.strip("[]"), int(port)
99
100
101def parse(argv):
102 opts, i = {}, 0
103 while i < len(argv) and argv[i] != "--":
104 opts[argv[i]] = argv[i + 1]
105 i += 2
106 cmd = argv[i + 1:]
107 if not cmd:
108 raise SystemExit("usage: --listen H:P --upstream H:P [--uid N --gid N] -- cmd ...")
109 return (addr(opts["--listen"]), addr(opts["--upstream"]),
110 int(opts.get("--uid", "65534")), int(opts.get("--gid", "65534")), cmd)
111
112
113def drop(uid, gid):
114 if os.geteuid() == 0:
115 os.setgroups([])
116 os.setgid(gid)
117 os.setuid(uid)
118 no_new_privs()
119
120
121def no_new_privs():
122 """prctl(PR_SET_NO_NEW_PRIVS): setuid/setgid bits and file capabilities
123 are ignored from here on (Linux only; a no-op elsewhere)."""
124 if not sys.platform.startswith("linux"):
125 return
126 import ctypes
127 libc = ctypes.CDLL(None, use_errno=True)
128 if libc.prctl(38, 1, 0, 0, 0) != 0:
129 raise OSError(ctypes.get_errno(), "prctl(PR_SET_NO_NEW_PRIVS)")
130
131
132def pump(src, dst):
133 total = 0
134 try:
135 while True:
136 data = src.recv(65536)
137 if not data:
138 break
139 total += len(data)
140 if total > MAX_BYTES:
141 log("connection over %d bytes, closed" % MAX_BYTES)
142 break
143 dst.sendall(data)
144 except OSError:
145 pass
146 for s in (src, dst):
147 try:
148 s.shutdown(socket.SHUT_RDWR)
149 except OSError:
150 pass
151
152
153def serve(conn, upstream, slots):
154 try:
155 try:
156 up = socket.create_connection(upstream, timeout=10)
157 except OSError as e:
158 log("upstream %s:%d: %s" % (upstream[0], upstream[1], e))
159 return
160 conn.settimeout(IDLE_SECS)
161 up.settimeout(IDLE_SECS)
162 back = threading.Thread(target=pump, args=(up, conn), daemon=True)
163 back.start()
164 pump(conn, up)
165 back.join(IDLE_SECS)
166 up.close()
167 finally:
168 conn.close()
169 slots.release()
170
171
172def accept_loop(srv, upstream):
173 slots = threading.BoundedSemaphore(MAX_CONNS)
174 while True:
175 conn, _ = srv.accept()
176 if not slots.acquire(blocking=False):
177 conn.close()
178 continue
179 threading.Thread(target=serve, args=(conn, upstream, slots), daemon=True).start()
180
181
182def main():
183 listen, upstream, uid, gid, cmd = parse(sys.argv[1:])
184 srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
185 srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
186 srv.bind(listen)
187 srv.listen(MAX_CONNS)
188 pid = os.fork()
189 if pid == 0:
190 try:
191 srv.close()
192 drop(uid, gid)
193 os.execv(cmd[0], cmd)
194 except BaseException as e:
195 log("cog exec failed: %s" % e)
196 os._exit(126)
197 drop(uid, gid)
198 for sig in (signal.SIGTERM, signal.SIGINT):
199 signal.signal(sig, lambda s, _f: os.kill(pid, s))
200 threading.Thread(target=accept_loop, args=(srv, upstream), daemon=True).start()
201 while True:
202 try:
203 _, status = os.waitpid(pid, 0)
204 break
205 except InterruptedError:
206 continue
207 code = os.waitstatus_to_exitcode(status)
208 sys.exit(code if code >= 0 else 128 - code)
209
210
211main()
212"#;