1/* 2 * auth_ldap.m vi:ts=4:sw=4:expandtab: 3 * OpenVPN LDAP Authentication Plugin 4 * 5 * Copyright (c) 2005 - 2007 Landon Fuller <landonf@threerings.net> 6 * Copyright (c) 2007 Three Rings Design, Inc. 7 * All rights reserved. 8 * 9 * Redistribution and use in source and binary forms, with or without 10 * modification, are permitted provided that the following conditions 11 * are met: 12 * 1. Redistributions of source code must retain the above copyright 13 * notice, this list of conditions and the following disclaimer. 14 * 2. Redistributions in binary form must reproduce the above copyright 15 * notice, this list of conditions and the following disclaimer in the 16 * documentation and/or other materials provided with the distribution. 17 * 3. Neither the name of Landon Fuller nor the names of any contributors 18 * may be used to endorse or promote products derived from this 19 * software without specific prior written permission. 20 * 21 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 24 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 25 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 26 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 27 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 28 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 29 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 30 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 31 * POSSIBILITY OF SUCH DAMAGE. 32 */ 33 34#import <err.h> 35#import <stdio.h> 36#import <stdlib.h> 37#import <stdarg.h> 38#import <errno.h> 39 40#import <ldap.h> 41 42#import <openvpn-plugin.h> 43 44#import <TRVPNPlugin.h> 45 46#include "openvpn-cr.h" 47 48/* Plugin Context */ 49typedef struct ldap_ctx { 50 TRAuthLDAPConfig *config; 51#ifdef HAVE_PF 52 id<TRPacketFilter> pf; 53#endif 54} ldap_ctx; 55 56 57static const char *get_env(const char *key, const char *env[]) { 58 int i; 59 60 if (!env) 61 return (NULL); 62 63 for (i = 0; env[i]; i++) { 64 size_t keylen = strlen(key); 65 66 if (keylen > strlen(env[i])) 67 continue; 68 69 if (!strncmp(key, env[i], keylen)) { 70 const char *p = env[i] + keylen; 71 if (*p == '=') 72 return (p + 1); 73 } 74 } 75 76 return (NULL); 77} 78 79static TRString *quoteForSearch(const char *string) { 80 const char specialChars[] = "*()\\"; /* RFC 2254. We don't care about NULL */ 81 TRString *result = [[TRString alloc] init]; 82 TRString *unquotedString, *part; 83 TRAutoreleasePool *pool = [[TRAutoreleasePool alloc] init]; 84 85 /* Make a copy of the string */ 86 unquotedString = [[TRString alloc] initWithCString: string]; 87 88 /* Initialize the result */ 89 result = [[TRString alloc] init]; 90 91 /* Quote all occurrences of the special characters */ 92 while ((part = [unquotedString substringToCharset: specialChars]) != NULL) { 93 TRString *temp; 94 size_t index; 95 char c; 96 97 /* Append everything until the first special character */ 98 [result appendString: part]; 99 100 /* Append the backquote */ 101 [result appendCString: "\\"]; 102 103 /* Get the special character */ 104 index = [unquotedString indexToCharset: specialChars]; 105 temp = [unquotedString substringFromIndex: index]; 106 c = [temp charAtIndex: 0]; 107 108 /* Append it, too! */ 109 [result appendChar: c]; 110 111 /* Move unquotedString past the special character */ 112 temp = [[unquotedString substringFromCharset: specialChars] retain]; 113 114 [unquotedString release]; 115 unquotedString = temp; 116 } 117 118 /* Append the remainder, if any */ 119 if (unquotedString) { 120 [result appendString: unquotedString]; 121 [unquotedString release]; 122 } 123 124 [pool release]; 125 126 return (result); 127} 128 129static TRString *createSearchFilter(TRString *template, const char *username) { 130 TRString *templateString; 131 TRString *result, *part; 132 TRString *quotedName; 133 const char userFormat[] = "%u"; 134 TRAutoreleasePool *pool = [[TRAutoreleasePool alloc] init]; 135 136 /* Copy the template */ 137 templateString = [[[TRString alloc] initWithString: template] autorelease]; 138 139 /* Initialize the result */ 140 result = [[TRString alloc] init]; 141 142 /* Quote the username */ 143 quotedName = quoteForSearch(username); 144 145 while ((part = [templateString substringToCString: userFormat]) != NULL) { 146 TRString *temp; 147 148 /* Append everything until the first %u */ 149 [result appendString: part]; 150 151 /* Append the username */ 152 [result appendString: quotedName]; 153 154 /* Move templateString past the %u */ 155 temp = [templateString substringFromCString: userFormat]; 156 templateString = temp; 157 } 158 159 [quotedName release]; 160 161 /* Append the remainder, if any */ 162 if (templateString) { 163 [result appendString: templateString]; 164 } 165 166 [pool release]; 167 168 return (result); 169} 170 171#ifdef HAVE_PF 172static BOOL pf_open(struct ldap_ctx *ctx) { 173 TRString *tableName; 174 TRLDAPGroupConfig *groupConfig; 175 TREnumerator *groupIter; 176 pferror_t pferror; 177 178 /* Acquire a reference to /dev/pf */ 179 ctx->pf = [[TRLocalPacketFilter alloc] init]; 180 if ((pferror = [ctx->pf open]) != PF_SUCCESS) { 181 /* /dev/pf could not be opened. Is it available? */ 182 [TRLog error: "Failed to open /dev/pf: %s", [TRPacketFilterUtil stringForError: pferror]]; 183 ctx->pf = nil; 184 return NO; 185 } 186 187 /* Clear out all referenced PF tables */ 188 if ((tableName = [ctx->config pfTable])) { 189 if ((pferror = [ctx->pf flushTable: tableName]) != PF_SUCCESS) { 190 [TRLog error: "Failed to clear packet filter table \"%s\": %s", [tableName cString], [TRPacketFilterUtil stringForError: pferror]]; 191 goto error; 192 } 193 } 194 195 if ([ctx->config ldapGroups]) { 196 groupIter = [[ctx->config ldapGroups] objectEnumerator]; 197 while ((groupConfig = [groupIter nextObject]) != nil) { 198 if ((tableName = [groupConfig pfTable])) { 199 if ((pferror = [ctx->pf flushTable: tableName]) != PF_SUCCESS) { 200 [TRLog error: "Failed to clear packet filter table \"%s\": %s", [tableName cString], [TRPacketFilterUtil stringForError: pferror]]; 201 goto error; 202 } 203 } 204 } 205 } 206 207 return YES; 208 209 error: 210 [ctx->pf release]; 211 ctx->pf = NULL; 212 return NO; 213} 214#endif /* HAVE_PF */ 215 216OPENVPN_EXPORT openvpn_plugin_handle_t 217openvpn_plugin_open_v1(unsigned int *type, const char *argv[], const char *envp[]) { 218 ldap_ctx *ctx = xmalloc(sizeof(ldap_ctx)); 219 220/* Read the configuration */ 221 ctx->config = [[TRAuthLDAPConfig alloc] initWithConfigFile: argv[1]]; 222 if (!ctx->config) { 223 free(ctx); 224 return (NULL); 225 } 226 227#ifdef HAVE_PF 228 ctx->pf = NULL; 229 /* Open reference to /dev/pf and clear out all of our PF tables */ 230 if ([ctx->config pfEnabled] && !pf_open(ctx)) { 231 [ctx->config release]; 232 free(ctx); 233 return (NULL); 234 } 235#endif 236 237 238 *type = OPENVPN_PLUGIN_MASK(OPENVPN_PLUGIN_AUTH_USER_PASS_VERIFY) | 239 OPENVPN_PLUGIN_MASK(OPENVPN_PLUGIN_CLIENT_CONNECT) | 240 OPENVPN_PLUGIN_MASK(OPENVPN_PLUGIN_CLIENT_DISCONNECT); 241 242 return (ctx); 243} 244 245OPENVPN_EXPORT void 246 openvpn_plugin_close_v1(openvpn_plugin_handle_t handle) 247{ 248 ldap_ctx *ctx = handle; 249 250 if (!ctx) 251 return; 252 253 /* Clean up the configuration file */ 254 [ctx->config release]; 255 256 /* Clean up PF */ 257#ifdef HAVE_PF 258 if (ctx->pf) 259 [ctx->pf release]; 260#endif 261 262 /* Finished */ 263 free(ctx); 264} 265 266TRLDAPConnection *connect_ldap(TRAuthLDAPConfig *config) { 267 TRLDAPConnection *ldap; 268 TRString *value; 269 270 /* Initialize our LDAP Connection */ 271 ldap = [[TRLDAPConnection alloc] initWithURL: [config url] timeout: [config timeout]]; 272 if (!ldap) { 273 [TRLog error: "Unable to open LDAP connection to %s\n", [[config url] cString]]; 274 return nil; 275 } 276 277 /* Referrals */ 278 if ([config referralEnabled]) { 279 if (![ldap setReferralEnabled: YES]) 280 goto error; 281 } else { 282 if (![ldap setReferralEnabled: NO]) 283 goto error; 284 } 285 286 /* Certificate file */ 287 if ((value = [config tlsCACertFile])) 288 if (![ldap setTLSCACertFile: value]) 289 goto error; 290 291 /* Certificate directory */ 292 if ((value = [config tlsCACertDir])) 293 if (![ldap setTLSCACertDir: value]) 294 goto error; 295 296 /* Client Certificate Pair */ 297 if ([config tlsCertFile] && [config tlsKeyFile]) 298 if(![ldap setTLSClientCert: [config tlsCertFile] keyFile: [config tlsKeyFile]]) 299 goto error; 300 301 /* Cipher suite */ 302 if ((value = [config tlsCipherSuite])) 303 if(![ldap setTLSCipherSuite: value]) 304 goto error; 305 306 /* Start TLS */ 307 if ([config tlsEnabled]) 308 if (![ldap startTLS]) 309 goto error; 310 311 /* Bind if requested */ 312 if ([config bindDN]) { 313 if (![ldap bindWithDN: [config bindDN] password: [config bindPassword]]) { 314 [TRLog error: "Unable to bind as %s", [[config bindDN] cString]]; 315 goto error; 316 } 317 } 318 319 return ldap; 320 321 error: 322 [ldap release]; 323 return nil; 324} 325 326static TRLDAPEntry *find_ldap_user (TRLDAPConnection *ldap, TRAuthLDAPConfig *config, const char *username) { 327 TRString *searchFilter; 328 TRArray *ldapEntries; 329 TRLDAPEntry *result = nil; 330 331 /* Assemble our search filter */ 332 searchFilter = createSearchFilter([config searchFilter], username); 333 334 /* Search! */ 335 ldapEntries = [ldap searchWithFilter: searchFilter 336 scope: LDAP_SCOPE_SUBTREE 337 baseDN: [config baseDN] 338 attributes: NULL]; 339 [searchFilter release]; 340 if (!ldapEntries) 341 return nil; 342 if ([ldapEntries count] < 1) { 343 return nil; 344 } 345 346 /* The specified search string may (but should not) return more than one entry. 347 * We ignore any extras. */ 348 result = [[ldapEntries lastObject] retain]; 349 350 return result; 351} 352 353 354static BOOL auth_ldap_user(TRLDAPConnection *ldap, TRAuthLDAPConfig *config, TRLDAPEntry *ldapUser, const char *password) { 355 TRLDAPConnection *authConn; 356 TRString *passwordString; 357 BOOL result = NO; 358 359 /* Create a second connection for binding */ 360 authConn = connect_ldap(config); 361 if (!authConn) { 362 return NO; 363 } 364 365 /* Allocate the string to pass to bindWithDN */ 366 passwordString = [[TRString alloc] initWithCString: password]; 367 368 if ([authConn bindWithDN: [ldapUser dn] password: passwordString]) { 369 result = YES; 370 } 371 372 [passwordString release]; 373 [authConn release]; 374 375 return result; 376} 377 378static TRLDAPGroupConfig *find_ldap_group(TRLDAPConnection *ldap, TRAuthLDAPConfig *config, TRLDAPEntry *ldapUser) { 379 TREnumerator *groupIter; 380 TRLDAPGroupConfig *groupConfig; 381 TRArray *ldapEntries; 382 TREnumerator *entryIter; 383 TRLDAPEntry *entry; 384 TRLDAPGroupConfig *result = nil; 385 int userNameLength; 386 387 /* 388 * Groups are loaded into the array in the order that they are listed 389 * in the configuration file, and we are expected to perform 390 * "first match". Thusly, we'll walk the stack from the bottom up. 391 */ 392 groupIter = [[config ldapGroups] objectReverseEnumerator]; 393 394 while ((groupConfig = [groupIter nextObject]) != nil) { 395 396 /* Search for the group */ 397 ldapEntries = [ldap searchWithFilter: [groupConfig searchFilter] 398 scope: LDAP_SCOPE_SUBTREE 399 baseDN: [groupConfig baseDN] 400 attributes: NULL]; 401 402 /* Error occured, all stop */ 403 if (!ldapEntries) 404 break; 405 406 /* If RFC2307BIS flag is true, search for full DN, otherwise just search for uid */ 407 TRString *searchValue = [groupConfig memberRFC2307BIS] ? [ldapUser dn] : [ldapUser rdn]; 408 409 /* This will be used if we're using the "search" operation instead of the "compare" operation */ 410 TRString *searchFilter = [TRString stringWithFormat: "(%s=%s)", [[groupConfig memberAttribute] cString], [searchValue cString]]; 411 412 /* Iterate over the returned entries */ 413 entryIter = [ldapEntries objectEnumerator]; 414 while ((entry = [entryIter nextObject]) != nil) { 415 if ((![groupConfig useCompareOperation] && [ldap searchWithFilter: searchFilter scope: LDAP_SCOPE_SUBTREE baseDN: [entry dn] attributes: NULL]) || 416 ([groupConfig useCompareOperation] && [ldap compareDN: [entry dn] withAttribute: [groupConfig memberAttribute] value: searchValue])) { 417 /* Group match! */ 418 result = groupConfig; 419 } 420 } 421 422 if (result) 423 break; 424 } 425 426 return result; 427} 428 429/** Handle user authentication. */ 430static int handle_auth_user_pass_verify(ldap_ctx *ctx, TRLDAPConnection *ldap, TRLDAPEntry *ldapUser, const char *password) { 431 TRLDAPGroupConfig *groupConfig; 432 433 const char *auth_password = password; 434 if ([ctx->config passWordIsCR]) { 435 openvpn_response resp; 436 char *parse_error; 437 if (!extract_openvpn_cr(password, &resp, &parse_error)) { 438 [TRLog error: "Error extracting challenge/response from password. Parse error = '%s'", parse_error]; 439 return (OPENVPN_PLUGIN_FUNC_ERROR); 440 } 441 auth_password = (const char*)resp.password; 442 } 443 444 /* Authenticate the user */ 445 if (!auth_ldap_user(ldap, ctx->config, ldapUser, auth_password)) { 446 [TRLog error: "Incorrect password supplied for LDAP DN \"%s\".", [[ldapUser dn] cString]]; 447 return (OPENVPN_PLUGIN_FUNC_ERROR); 448 } 449 450 /* User authenticated, find group, if any */ 451 if ([ctx->config ldapGroups]) { 452 groupConfig = find_ldap_group(ldap, ctx->config, ldapUser); 453 if (!groupConfig && [ctx->config requireGroup]) { 454 /* No group match, and group membership is required */ 455 return OPENVPN_PLUGIN_FUNC_ERROR; 456 } else { 457 /* Group match! */ 458 return OPENVPN_PLUGIN_FUNC_SUCCESS; 459 } 460 } else { 461 // No groups, user OK 462 return OPENVPN_PLUGIN_FUNC_SUCCESS; 463 } 464 465 /* Never reached */ 466 return OPENVPN_PLUGIN_FUNC_ERROR; 467} 468 469#ifdef HAVE_PF 470/* Add (or remove) the remote address */ 471static BOOL pf_client_connect_disconnect(struct ldap_ctx *ctx, TRString *tableName, const char *remoteAddress, BOOL connecting) { 472 TRString *addressString; 473 TRPFAddress *address; 474 pferror_t pferror; 475 476 addressString = [[TRString alloc] initWithCString: remoteAddress]; 477 address = [[TRPFAddress alloc] initWithPresentationAddress: addressString]; 478 [addressString release]; 479 if (connecting) { 480 [TRLog debug: "Adding address \"%s\" to packet filter table \"%s\".", remoteAddress, [tableName cString]]; 481 482 if ((pferror = [ctx->pf addAddress: address toTable: tableName]) != PF_SUCCESS) { 483 [TRLog error: "Failed to add address \"%s\" to table \"%s\": %s", remoteAddress, [tableName cString], [TRPacketFilterUtil stringForError: pferror]]; 484 [address release]; 485 return NO; 486 } 487 } else { 488 [TRLog debug: "Removing address \"%s\" from packet filter table \"%s\".", remoteAddress, [tableName cString]]; 489 if ((pferror = [ctx->pf deleteAddress: address fromTable: tableName]) != PF_SUCCESS) { 490 [TRLog error: "Failed to remove address \"%s\" from table \"%s\": %s", 491 remoteAddress, [tableName cString], [TRPacketFilterUtil stringForError: pferror]]; 492 [address release]; 493 return NO; 494 } 495 } 496 [address release]; 497 498 return YES; 499} 500#endif /* HAVE_PF */ 501 502 503/** Handle both connection and disconnection events. */ 504static int handle_client_connect_disconnect(ldap_ctx *ctx, TRLDAPConnection *ldap, TRLDAPEntry *ldapUser, const char *remoteAddress, BOOL connecting) { 505 TRLDAPGroupConfig *groupConfig = nil; 506#ifdef HAVE_PF 507 TRString *tableName = nil; 508#endif 509 510 /* Locate the group (config), if any */ 511 if ([ctx->config ldapGroups]) { 512 groupConfig = find_ldap_group(ldap, ctx->config, ldapUser); 513 if (!groupConfig && [ctx->config requireGroup]) { 514 [TRLog error: "No matching LDAP group found for user DN \"%s\", and group membership is required.", [[ldapUser dn] cString]]; 515 /* No group match, and group membership is required */ 516 return OPENVPN_PLUGIN_FUNC_ERROR; 517 } 518 } 519 520#ifdef HAVE_PF 521 /* Grab the requested PF table name, if any */ 522 if (groupConfig) { 523 tableName = [groupConfig pfTable]; 524 } else { 525 tableName = [ctx->config pfTable]; 526 } 527 528 if (tableName) 529 if (!pf_client_connect_disconnect(ctx, tableName, remoteAddress, connecting)) 530 return OPENVPN_PLUGIN_FUNC_ERROR; 531#endif /* HAVE_PF */ 532 533 return OPENVPN_PLUGIN_FUNC_SUCCESS; 534} 535 536 537 538OPENVPN_EXPORT int 539openvpn_plugin_func_v1(openvpn_plugin_handle_t handle, const int type, const char *argv[], const char *envp[]) { 540 const char *username, *password, *remoteAddress; 541 ldap_ctx *ctx = handle; 542 TRLDAPConnection *ldap = nil; 543 TRLDAPEntry *ldapUser = nil; 544 TRAutoreleasePool *pool = nil; 545 int ret = OPENVPN_PLUGIN_FUNC_ERROR; 546 547 /* Per-request allocation pool. */ 548 pool = [[TRAutoreleasePool alloc] init]; 549 550 username = get_env("username", envp); 551 TRString *userName=[[TRString alloc]initWithCString: username]; 552 password = get_env("password", envp); 553 remoteAddress = get_env("ifconfig_pool_remote_ip", envp); 554 555 556 /* At the very least, we need a username to work with */ 557 if (!username) { 558 [TRLog debug: "No remote username supplied to OpenVPN LDAP Plugin."]; 559 goto cleanup; 560 } 561 562 /* Create an LDAP connection */ 563 if (!(ldap = connect_ldap(ctx->config))) { 564 [TRLog error: "LDAP connect failed."]; 565 goto cleanup; 566 } 567 568 /* Find the user record */ 569 ldapUser = find_ldap_user(ldap, ctx->config, username); 570 [ldapUser setRDN: userName]; 571 if (!ldapUser) { 572 /* No such user. */ 573 [TRLog warning: "LDAP user \"%s\" was not found.", username]; 574 goto cleanup; 575 } 576 577 switch (type) { 578 /* Password Authentication */ 579 case OPENVPN_PLUGIN_AUTH_USER_PASS_VERIFY: 580 if (!password) { 581 [TRLog debug: "No remote password supplied to OpenVPN LDAP Plugin (OPENVPN_PLUGIN_AUTH_USER_PASS_VERIFY)."]; 582 } else { 583 ret = handle_auth_user_pass_verify(ctx, ldap, ldapUser, password); 584 } 585 break; 586 /* New connection established */ 587 case OPENVPN_PLUGIN_CLIENT_CONNECT: 588 if (!remoteAddress) { 589 [TRLog debug: "No remote address supplied to OpenVPN LDAP Plugin (OPENVPN_PLUGIN_CLIENT_CONNECT)."]; 590 } else { 591 ret = handle_client_connect_disconnect(ctx, ldap, ldapUser, remoteAddress, YES); 592 } 593 break; 594 case OPENVPN_PLUGIN_CLIENT_DISCONNECT: 595 if (!remoteAddress) { 596 [TRLog debug: "No remote address supplied to OpenVPN LDAP Plugin (OPENVPN_PLUGIN_CLIENT_DISCONNECT)."]; 597 } else { 598 ret = handle_client_connect_disconnect(ctx, ldap, ldapUser, remoteAddress, NO); 599 } 600 break; 601 default: 602 [TRLog debug: "Unhandled plugin type in OpenVPN LDAP Plugin (type=%d)", type]; 603 break; 604 } 605 606cleanup: 607 if (ldapUser != nil) 608 [ldapUser release]; 609 610 if (ldap != nil) 611 [ldap release]; 612 613 if (pool != nil) 614 [pool release]; 615 616 return (ret); 617} 618