1 /*
2 * auth-script OpenVPN plugin
3 *
4 * Runs an external script to decide whether to authenticate a user or not.
5 * Useful for checking 2FA on VPN auth attempts as it doesn't block the main
6 * openvpn process, unlike passing the script to --auth-user-pass-verify.
7 *
8 * Functions required to be a valid OpenVPN plugin:
9 * openvpn_plugin_open_v3
10 * openvpn_plugin_func_v3
11 * openvpn_plugin_close_v1
12 */
13
14 /* Required to use strdup */
15 #define __EXTENSIONS__
16
17 /********** Includes */
18 #include <stddef.h>
19 #include <errno.h>
20 #include <openvpn-plugin.h>
21 #include <stdio.h>
22 #include <stdlib.h>
23 #include <string.h>
24 #include <unistd.h>
25 #include <sys/wait.h>
26 #include <sys/stat.h>
27
28 /********** Constants */
29 /* For consistency in log messages */
30 #define PLUGIN_NAME "auth-script"
31 #define OPENVPN_PLUGIN_VERSION_MIN 3
32 #define SCRIPT_NAME_IDX 0
33
34 /* Where we store our own settings/state */
35 struct plugin_context
36 {
37 plugin_log_t plugin_log;
38 const char *argv[];
39 };
40
41 /* Handle an authentication request */
deferred_handler(struct plugin_context * context,const char * envp[])42 static int deferred_handler(struct plugin_context *context,
43 const char *envp[])
44 {
45 plugin_log_t log = context->plugin_log;
46 pid_t pid;
47
48 log(PLOG_DEBUG, PLUGIN_NAME,
49 "Deferred handler using script_path=%s",
50 context->argv[SCRIPT_NAME_IDX]);
51
52 pid = fork();
53
54 /* Parent - child failed to fork */
55 if (pid < 0) {
56 log(PLOG_ERR, PLUGIN_NAME,
57 "pid failed < 0 check, got %d", pid);
58 return OPENVPN_PLUGIN_FUNC_ERROR;
59 }
60
61 /* Parent - child forked successfully
62 *
63 * Here we wait until that child completes before notifying OpenVPN of
64 * our status.
65 */
66 if (pid > 0) {
67 pid_t wait_rc;
68 int wstatus;
69
70 log(PLOG_DEBUG, PLUGIN_NAME, "child pid is %d", pid);
71
72 /* Block until the child returns */
73 wait_rc = waitpid(pid, &wstatus, 0);
74
75 /* Values less than 0 indicate no child existed */
76 if (wait_rc < 0) {
77 log(PLOG_ERR, PLUGIN_NAME,
78 "wait failed for pid %d, waitpid got %d",
79 pid, wait_rc);
80 return OPENVPN_PLUGIN_FUNC_ERROR;
81 }
82
83 /* WIFEXITED will be true if the child exited normally, any
84 * other return indicates an abnormal termination.
85 */
86 if (WIFEXITED(wstatus)) {
87 log(PLOG_DEBUG, PLUGIN_NAME,
88 "child pid %d exited with status %d",
89 pid, WEXITSTATUS(wstatus));
90 return WEXITSTATUS(wstatus);
91 }
92
93 log(PLOG_ERR, PLUGIN_NAME,
94 "child pid %d terminated abnormally",
95 pid);
96 return OPENVPN_PLUGIN_FUNC_ERROR;
97 }
98
99
100 /* Child Control - Spin off our sucessor */
101 pid = fork();
102
103 /* Notify our parent that our child faild to fork */
104 if (pid < 0)
105 exit(OPENVPN_PLUGIN_FUNC_ERROR);
106
107 /* Let our parent know that our child is working appropriately */
108 if (pid > 0)
109 exit(OPENVPN_PLUGIN_FUNC_DEFERRED);
110
111 /* Child Spawn - This process actually spawns the script */
112
113 /* Daemonize */
114 umask(0);
115 setsid();
116
117 /* Close open files and move to root */
118 int chdir_rc = chdir("/");
119 if (chdir_rc < 0)
120 log(PLOG_DEBUG, PLUGIN_NAME,
121 "Error trying to change pwd to \'/\'");
122 close(STDIN_FILENO);
123 close(STDOUT_FILENO);
124 close(STDERR_FILENO);
125
126 int execve_rc = execve(context->argv[0],
127 (char *const*)context->argv,
128 (char *const*)envp);
129 if ( execve_rc == -1 ) {
130 switch(errno) {
131 case E2BIG:
132 log(PLOG_DEBUG, PLUGIN_NAME,
133 "Error trying to exec: E2BIG");
134 break;
135 case EACCES:
136 log(PLOG_DEBUG, PLUGIN_NAME,
137 "Error trying to exec: EACCES");
138 break;
139 case EAGAIN:
140 log(PLOG_DEBUG, PLUGIN_NAME,
141 "Error trying to exec: EAGAIN");
142 break;
143 case EFAULT:
144 log(PLOG_DEBUG, PLUGIN_NAME,
145 "Error trying to exec: EFAULT");
146 break;
147 case EINTR:
148 log(PLOG_DEBUG, PLUGIN_NAME,
149 "Error trying to exec: EINTR");
150 break;
151 case EINVAL:
152 log(PLOG_DEBUG, PLUGIN_NAME,
153 "Error trying to exec: EINVAL");
154 break;
155 case ELOOP:
156 log(PLOG_DEBUG, PLUGIN_NAME,
157 "Error trying to exec: ELOOP");
158 break;
159 case ENAMETOOLONG:
160 log(PLOG_DEBUG, PLUGIN_NAME,
161 "Error trying to exec: ENAMETOOLONG");
162 break;
163 case ENOENT:
164 log(PLOG_DEBUG, PLUGIN_NAME,
165 "Error trying to exec: ENOENT");
166 break;
167 case ENOEXEC:
168 log(PLOG_DEBUG, PLUGIN_NAME,
169 "Error trying to exec: ENOEXEC");
170 break;
171 case ENOLINK:
172 log(PLOG_DEBUG, PLUGIN_NAME,
173 "Error trying to exec: ENOLINK");
174 break;
175 case ENOMEM:
176 log(PLOG_DEBUG, PLUGIN_NAME,
177 "Error trying to exec: ENOMEM");
178 break;
179 case ENOTDIR:
180 log(PLOG_DEBUG, PLUGIN_NAME,
181 "Error trying to exec: ENOTDIR");
182 break;
183 case ETXTBSY:
184 log(PLOG_DEBUG, PLUGIN_NAME,
185 "Error trying to exec: ETXTBSY");
186 break;
187 default:
188 log(PLOG_ERR, PLUGIN_NAME,
189 "Error trying to exec: unknown, errno: %d",
190 errno);
191 }
192 }
193 exit(EXIT_FAILURE);
194 }
195
196 /* We require OpenVPN Plugin API v3 */
openvpn_plugin_min_version_required_v1()197 OPENVPN_EXPORT int openvpn_plugin_min_version_required_v1()
198 {
199 return OPENVPN_PLUGIN_VERSION_MIN;
200 }
201
202 /*
203 * Handle plugin initialization
204 * arguments->argv[0] is path to shared lib
205 * arguments->argv[1] is expected to be path to script
206 */
openvpn_plugin_open_v3(const int struct_version,struct openvpn_plugin_args_open_in const * arguments,struct openvpn_plugin_args_open_return * retptr)207 OPENVPN_EXPORT int openvpn_plugin_open_v3(const int struct_version,
208 struct openvpn_plugin_args_open_in const *arguments,
209 struct openvpn_plugin_args_open_return *retptr)
210 {
211 plugin_log_t log = arguments->callbacks->plugin_log;
212 log(PLOG_DEBUG, PLUGIN_NAME, "FUNC: openvpn_plugin_open_v3");
213
214 struct plugin_context *context = NULL;
215
216 /* Safeguard on openvpn versions */
217 if (struct_version < OPENVPN_PLUGINv3_STRUCTVER) {
218 log(PLOG_ERR, PLUGIN_NAME,
219 "ERROR: struct version was older than required");
220 return OPENVPN_PLUGIN_FUNC_ERROR;
221 }
222
223 /* Tell OpenVPN we want to handle these calls */
224 retptr->type_mask = OPENVPN_PLUGIN_MASK(
225 OPENVPN_PLUGIN_AUTH_USER_PASS_VERIFY);
226
227
228 /*
229 * Determine the size of the arguments provided so we can allocate and
230 * argv array of appropriate length.
231 */
232 size_t arg_size = 0;
233 for (int arg_idx = 1; arguments->argv[arg_idx]; arg_idx++)
234 arg_size += strlen(arguments->argv[arg_idx]);
235
236
237 /*
238 * Plugin init will fail unless we create a handler, so we'll store our
239 * script path and it's arguments there as we have to create it anyway.
240 */
241 context = (struct plugin_context *) malloc(
242 sizeof(struct plugin_context) + arg_size);
243 memset(context, 0, sizeof(struct plugin_context) + arg_size);
244 context->plugin_log = log;
245
246
247 /*
248 * Check we've been handed a script path to call
249 * This comes directly from openvpn config file:
250 * plugin /path/to/auth.so /path/to/auth/script.sh
251 *
252 * IDX 0 should correspond to the library, IDX 1 should be the
253 * script, and any subsequent entries should be arguments to the script.
254 *
255 * Note that if arg_size is 0 no script argument was included.
256 */
257 if (arg_size > 0) {
258 memcpy(&context->argv, &arguments->argv[1], arg_size);
259
260 log(PLOG_DEBUG, PLUGIN_NAME,
261 "script_path=%s",
262 context->argv[SCRIPT_NAME_IDX]);
263 } else {
264 free(context);
265 log(PLOG_ERR, PLUGIN_NAME,
266 "ERROR: no script_path specified in config file");
267 return OPENVPN_PLUGIN_FUNC_ERROR;
268 }
269
270 /* Pass state back to OpenVPN so we get handed it back later */
271 retptr->handle = (openvpn_plugin_handle_t) context;
272
273 log(PLOG_DEBUG, PLUGIN_NAME, "plugin initialized successfully");
274
275 return OPENVPN_PLUGIN_FUNC_SUCCESS;
276 }
277
278 /* Called when we need to handle OPENVPN_PLUGIN_AUTH_USER_PASS_VERIFY calls */
openvpn_plugin_func_v3(const int struct_version,struct openvpn_plugin_args_func_in const * arguments,struct openvpn_plugin_args_func_return * retptr)279 OPENVPN_EXPORT int openvpn_plugin_func_v3(const int struct_version,
280 struct openvpn_plugin_args_func_in const *arguments,
281 struct openvpn_plugin_args_func_return *retptr)
282 {
283 (void)retptr; /* Squish -Wunused-parameter warning */
284 struct plugin_context *context =
285 (struct plugin_context *) arguments->handle;
286 plugin_log_t log = context->plugin_log;
287
288 log(PLOG_DEBUG, PLUGIN_NAME, "FUNC: openvpn_plugin_func_v3");
289
290 /* Safeguard on openvpn versions */
291 if (struct_version < OPENVPN_PLUGINv3_STRUCTVER) {
292 log(PLOG_ERR, PLUGIN_NAME,
293 "ERROR: struct version was older than required");
294 return OPENVPN_PLUGIN_FUNC_ERROR;
295 }
296
297 if(arguments->type == OPENVPN_PLUGIN_AUTH_USER_PASS_VERIFY) {
298 log(PLOG_DEBUG, PLUGIN_NAME,
299 "Handling auth with deferred script");
300 return deferred_handler(context, arguments->envp);
301 } else
302 return OPENVPN_PLUGIN_FUNC_SUCCESS;
303 }
304
openvpn_plugin_close_v1(openvpn_plugin_handle_t handle)305 OPENVPN_EXPORT void openvpn_plugin_close_v1(openvpn_plugin_handle_t handle)
306 {
307 struct plugin_context *context = (struct plugin_context *) handle;
308 free(context);
309 }
310