diff -r e253b4383132 -r 1a5334dfb21d tunblick/openvpnstart.m --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tunblick/openvpnstart.m Wed Jul 29 11:23:17 2009 +0200 @@ -0,0 +1,330 @@ +/* + * Copyright (c) 2004-2006 Angelo Laub + * Contributions by Dirk Theisen + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program (see the file COPYING included with this + * distribution); if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#import +#import +#import +#import +#import +#import +#import + +void startVPN (NSString* configFile, int port, BOOL useScripts, BOOL skipCheck); //Tries to start an openvpn connection. May complain and exit if can't become root +void killOneOpenvpn (pid_t pid); //Returns having killed an openvpn process, or complains and exits +int killAllOpenvpn (void); //Kills all openvpn processes and returns the number of processes that were killed. May complain and exit + +void loadKexts (void); //Tries to load kexts -- no indication of failure. May complain and exit if can't become root +void becomeRoot (void); //Returns as root, having setuid(0) if necessary; complains and exits if can't become root + +void getProcesses (struct kinfo_proc** procs, int* number); //Fills in process information +BOOL isOpenvpn (pid_t pid); //Returns TRUE if process is an openvpn process (i.e., process name = "openvpn") +BOOL configNeedsRepair (void); //Returns NO if configuration file is secure, otherwise complains and exits + +NSString* execPath; //Path to folder containing this executable, openvpn, tap.kext, tun.kext, client.up.osx.sh, and client.down.osx.sh +NSString* configPath; //Path to configuration file (in ~/Library/openvpn) +NSAutoreleasePool* pool; + +int main(int argc, char* argv[]) +{ + pool = [[NSAutoreleasePool alloc] init]; + + BOOL syntaxError = TRUE; + + if (argc > 1) { + char* command = argv[1]; + if( strcmp(command, "killall") == 0 ) { + if (argc == 2) { + int nKilled; + nKilled = killAllOpenvpn(); + if (nKilled) { + printf("%d openvpn processes killed\n", nKilled); + } else { + fprintf(stderr, "%d openvpn processes killed\n", nKilled); + } + syntaxError = FALSE; + } + } else if( strcmp(command, "kill") == 0 ) { + if (argc == 3) { + pid_t pid = (pid_t) atoi(argv[2]); + killOneOpenvpn(pid); + syntaxError = FALSE; + } + } else if( strcmp(command, "start") == 0 ) { + if ( (argc > 3) && (argc < 7) ) { + NSString* configFile = [NSString stringWithUTF8String:argv[2]]; + execPath = [[NSString stringWithUTF8String:argv[0]] stringByDeletingLastPathComponent]; + if(strlen(argv[3]) < 6 ) { + unsigned int port = atoi(argv[3]); + if (port<=65535) { + BOOL useScripts = FALSE; if( (argc > 4) && (atoi(argv[4]) == 1) ) useScripts = TRUE; + BOOL skipCheck = FALSE; if( (argc > 5) && (atoi(argv[5]) == 1) ) skipCheck = TRUE; + startVPN(configFile, port, useScripts, skipCheck); + syntaxError = FALSE; + } + } + } + } + } + + if (syntaxError) { + fprintf(stderr, "Error: Syntax error. Usage:\n\n" + + "\t./openvpnstart killall\n" + "\t./openvpnstart kill processId\n" + "\t./openvpnstart start configName mgtPort [useScripts [skipCheck] ]\n\n" + + "Where:\n" + "\tprocessId is the process ID of the openvpn process to kill\n" + "\tconfigName is the name of the configuration file (which must be in ~/Library/openvpn)\n" + "\tmgtPort is the port number (0-65535) to use for managing the connection\n" + "\tuseScripts is 1 to run the client.up.osx.sh script before connecting, and client.down.osx.sh after disconnecting\n" + "\t (The scripts are in Tunnelblick.app/Contents/Resources/)\n" + "\tskipCheck is 1 to skip checking ownership and permissions of the configuration file\n\n" + + "useScripts and skipCheck each default to 0.\n\n" + + "The normal return code is 0. If an error occurs a message is sent to stderr and a code of 2 is returned.\n" + + "This executable must be in the same folder as openvpn, tap.kext, and tun.kext (and client.up.osx.sh and client.down.osx.sh if they are used).\n\n" + + "Tunnelblick must have been run and an administrator password entered at least once before openvpnstart can be used." + ); + [pool drain]; + exit(2); + } + + [pool drain]; + exit(0); +} + +//Tries to start an openvpn connection -- no indication of failure. May complain and exit if can't become root +void startVPN(NSString* configFile, int port, BOOL useScripts, BOOL skipCheck) +{ + NSString* directoryPath = [NSHomeDirectory() stringByAppendingPathComponent:@"/Library/openvpn"]; + configPath = [directoryPath stringByAppendingPathComponent:configFile]; + NSString* openvpnPath = [execPath stringByAppendingPathComponent: @"openvpn"]; + NSMutableString* upscriptPath = [[execPath stringByAppendingPathComponent: @"client.up.osx.sh"] mutableCopy]; + NSMutableString* downscriptPath = [[execPath stringByAppendingPathComponent: @"client.down.osx.sh"] mutableCopy]; + [upscriptPath replaceOccurrencesOfString:@" " withString:@"\\ " options:NSLiteralSearch range:NSMakeRange(0, [upscriptPath length])]; + [downscriptPath replaceOccurrencesOfString:@" " withString:@"\\ " options:NSLiteralSearch range:NSMakeRange(0, [downscriptPath length])]; + + if ( ! skipCheck ) { + if(configNeedsRepair()) { + [pool drain]; + exit(2); + } + } + + // default arguments to openvpn command line + NSMutableArray* arguments = [NSMutableArray arrayWithObjects: + @"--management-query-passwords", + @"--cd", directoryPath, + @"--daemon", + @"--management-hold", + @"--management", @"127.0.0.1", [NSString stringWithFormat:@"%d", port], + @"--config", configPath, + @"--script-security", @"2", // allow us to call the up and down scripts or scripts defined in config + nil + ]; + + // conditionally push additional arguments to array + if(useScripts) { + [arguments addObjectsFromArray: + [NSArray arrayWithObjects: + @"--up", upscriptPath, + @"--down", downscriptPath, + @"--up-restart", + nil + ] + ]; + } + + loadKexts(); + + NSTask* task = [[[NSTask alloc] init] autorelease]; + [task setLaunchPath:openvpnPath]; + [task setArguments:arguments]; + + becomeRoot(); + [task launch]; + [task waitUntilExit]; + + [upscriptPath release]; + [downscriptPath release]; +} + +//Returns having killed an openvpn process, or complains and exits +void killOneOpenvpn(pid_t pid) +{ + int didnotKill; + + if(isOpenvpn(pid)) { + becomeRoot(); + didnotKill = kill(pid, SIGTERM); + if (didnotKill) { + fprintf(stderr, "Error: Unable to kill openvpn process %d\n", pid); + [pool drain]; + exit(2); + } + } else { + fprintf(stderr, "Error: Process %d is not an openvpn process\n", pid); + [pool drain]; + exit(2); + } +} + +//Kills all openvpn processes and returns the number of processes that were killed. May complain and exit if can't become root or some openvpn processes can't be killed +int killAllOpenvpn(void) +{ + int count = 0, + i = 0, + nKilled = 0, //# of openvpn processes succesfully killed + nNotKilled = 0, //# of openvpn processes not killed + didnotKill; //return value from kill() -- zero indicates killed successfully + + struct kinfo_proc* info = NULL; + + getProcesses(&info, &count); + + for (i = 0; i < count; i++) { + char* process_name = info[i].kp_proc.p_comm; + pid_t pid = info[i].kp_proc.p_pid; + if(strcmp(process_name, "openvpn") == 0) { + becomeRoot(); + didnotKill = kill(pid, SIGTERM); + if (didnotKill) { + fprintf(stderr, "Error: Unable to kill openvpn process %d\n", pid); + nNotKilled++; + } else { + nKilled++; + } + } + } + + free(info); + + if (nNotKilled) { + // An error message for each openvpn process that wasn't killed has already been output + [pool drain]; + exit(2); + } + + return(nKilled); +} + +//Tries to load kexts -- no indication of failure. May complain and exit if can't become root +void loadKexts(void) +{ + NSString* tapPath = [execPath stringByAppendingPathComponent: @"tap.kext"]; + NSString* tunPath = [execPath stringByAppendingPathComponent: @"tun.kext"]; + NSTask* task = [[[NSTask alloc] init] autorelease]; + NSArray* arguments = [NSArray arrayWithObjects:tapPath, tunPath, nil]; + + [task setLaunchPath:@"/sbin/kextload"]; + + [task setArguments:arguments]; + + becomeRoot(); + [task launch]; + [task waitUntilExit]; +} + +//Returns as root, having setuid(0) if necessary; complains and exits if can't become root +void becomeRoot(void) +{ + if (getuid() != 0) { + if ( setuid(0) ) { + fprintf(stderr, "Error: Unable to become root\n" + "You must have run Tunnelblick and entered an administrator password at least once to use openvpnstart\n"); + [pool drain]; + exit(2); + } + } +} + +//Fills in process information +void getProcesses(struct kinfo_proc** procs, int* number) +{ + int mib[4] = { CTL_KERN, KERN_PROC, KERN_PROC_ALL, 0 }; + struct kinfo_proc* info; + size_t length; + int level = 3; + + if (sysctl(mib, level, NULL, &length, NULL, 0) < 0) return; + if (!(info = malloc(length))) return; + if (sysctl(mib, level, info, &length, NULL, 0) < 0) { + free(info); + return; + } + *procs = info; + *number = length / sizeof(struct kinfo_proc); +} + +//Returns TRUE if process is an openvpn process (i.e., process name = "openvpn"), otherwise returns FALSE +BOOL isOpenvpn(pid_t pid) +{ + BOOL is_openvpn = FALSE; + int count = 0, + i = 0; + struct kinfo_proc* info = NULL; + + getProcesses(&info, &count); + for (i = 0; i < count; i++) { + char* process_name = info[i].kp_proc.p_comm; + pid_t thisPid = info[i].kp_proc.p_pid; + if (pid == thisPid) { + if (strcmp(process_name, "openvpn")==0) { + is_openvpn = TRUE; + } else { + is_openvpn = FALSE; + } + break; + } + } + free(info); + return is_openvpn; +} + +//Returns NO if configuration file is secure, otherwise complains and exits +BOOL configNeedsRepair(void) +{ + NSFileManager* fileManager = [NSFileManager defaultManager]; + NSDictionary* fileAttributes = [fileManager fileAttributesAtPath:configPath traverseLink:YES]; + + if (fileAttributes == nil) { + fprintf(stderr, "Error: %s does not exist\n", [configPath UTF8String]); + [pool drain]; + exit(2); + } + + unsigned long perms = [fileAttributes filePosixPermissions]; + NSString* octalString = [NSString stringWithFormat:@"%o", perms]; + NSNumber* fileOwner = [fileAttributes fileOwnerAccountID]; + + if ( (![octalString isEqualToString:@"644"]) || (![fileOwner isEqualToNumber:[NSNumber numberWithInt:0]])) { + NSString* errMsg = [NSString stringWithFormat:@"Error: File %@ is owned by %@ and has permissions %@\n" + "Configuration files must be owned by root:wheel with permissions 0644\n" + "To skip this check, use 'skipCheck' -- type './openvpnstart' (with no arguments) for details)\n", + configPath, fileOwner, octalString]; + fprintf(stderr, [errMsg UTF8String]); + [pool drain]; + exit(2); + } + return NO; +}