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