Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
openSUSE:12.2
pptp
pptp-command
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File pptp-command of Package pptp
#!/usr/bin/perl -wT #this is a combination of jeff's previous pptp scripts #functions: # setup - configures tunnel servers and chap-secrets # start - brings up a tunnel # stop - brings down a tunnel # # chkconfig: - 90 10 # description: cleanly brings down the tunnel when changing runlevels. # ### BEGIN INIT INFO # Provides: pptp # Required-Start: $network # Required-Stop: $network # Default-Start: # Default-Stop: 0 1 2 3 4 5 6 # Description: PPTP based VPN ### END INIT INFO # $Id: pptp-command,v 1.12 2001/12/20 13:09:02 jwiedemeier Exp $ ####### # Data # # the regexp for the list of characters that are unsafe # to put inside a system() or `` # it is built by saying everything but known safe characters # anyone want to make bets on if this holds true for i18n'ed systems? my $safe_set = '-A-Za-z0-9\s\._\/:'; my $unsafe_re = "[^$safe_set]"; my $safe_re = "[$safe_set]*"; # # pppdir - the directory containing the ppp config files # my $pppdir = $ENV{"PPPDIR"}; die "Stop screwing with me and set PPPDIR to something reasonable\n" if defined $pppdir && $pppdir =~ /$unsafe_re/o; $pppdir = "/etc/ppp" unless defined $pppdir; # # pptpdir - the directory containing the pptp drop-in config files # my $pptpdir = $ENV{"PPTPDIR"}; die "Stop screwing with me and set PPTPDIR to something reasonable\n" if defined $pptpdir && $pptpdir =~ /$unsafe_re/o; $pptpdir = "/etc/pptp.d" unless defined $pptpdir; # # chap_secrets - the full path to the the CHAP # (Challenge/Handshake Authentication Protocol) secrets file # my $chap_secrets = "$pppdir/chap-secrets"; my $pap_secrets = "$pppdir/pap-secrets"; # # tunnel_dir - the directory containing tunnel config files # my $tunnel_dir = "$pppdir/peers"; # # subsys_dir - the place "rc" looks to see if a servics is started # before it runs the K* scripts my $subsys_dir = "/var/lock/subsys"; # # The resolv.confs... # my $resolv = "/etc/resolv.conf"; my $resolv_pptp = "$resolv.pptp"; my $resolv_real = "$resolv.real"; # # clean up the path since this is run as root. $ENV{PATH} = "/bin:/usr/bin:/usr/sbin"; delete $ENV{BASH_ENV}; delete $ENV{IFS}; delete $ENV{ENV}; sub usage() { print "usage: $0 [setup|stop|start [tunnel]]\n"; print "all options must be specified to run non-interactively\n"; exit 1; } ####### #first some support functions that are used everywhere # #yesno <prompt> # # Ask the user <prompt> and return true for yes, false for no # sub yesno($) { my $prompt = $_[0]; while(1) { print "\n$prompt [Y/n]:"; my $choice = <STDIN>; chomp $choice; return 1 if $choice eq "" || $choice =~ /[Yy]/; return 0 if $choice =~ /[Nn]/; print "\nI don't understand '$choice', please try again...\n"; } } #QueryUser <prompt> <default> # # Ask the user <prompt> and return the answer, <default> if cr # sub QueryUser($$) { my ($prompt, $default) = @_; print "$prompt"; print " [$default]" if defined $default; print ": "; my $answer = <STDIN>; chomp $answer; $answer = $default if $answer eq "" and defined $default; return $answer; } #ConfiguredTunnels # # Returns a list of configured tunnels # sub ConfiguredTunnels() { my @tunnels = (); if( -d "$tunnel_dir" ) { foreach my $f (`cd $tunnel_dir; ls`) { chomp $f; next if $f eq "__default"; my $p = "$tunnel_dir/$f"; if( $p !~ /^($safe_re)$/o ) { print "Unsafe characters in tunnel name $p\n"; next; } $p = $1; push @tunnels, $f if -f $p and `grep '# PPTP' $p`; } } return @tunnels; } #bselect # # a rough equilivent of the bourne shell's select sub bselect($@) { my $prompt = shift; my @choices = @_; for my $i (0..$#choices) { print $i+1 .".) $choices[$i]\n"; } my $reply = QueryUser $prompt, undef; return $reply; } #SelectTunnel - interactive # # Prints $_[0] as a prompt and returns the choice. # sub SelectTunnel($) { my $tunnel = ""; my @tunnels = ConfiguredTunnels; while($tunnel eq "") { $tunnel = bselect $_[0], @tunnels; } return $tunnels[$tunnel - 1] if $tunnel =~ /^\d+$/; return $tunnel if grep {/$tunnel/} @tunnels; return ""; } #AddTunnel <name> <ip> <local> <remote> # # Adds a new tunnel with name <name>, server ip address <ip>, # and using the CHAP secret determined by local name <local> and remote # name <remote>. sub AddTunnel($$$$@) { my ($name, $ip, $local, $remote, @routes) = @_; if( -f "$tunnel_dir/$name") { print "ERROR! Peer $name already exists!\n"; return; } open(PEER, ">$tunnel_dir/$name") or die "can't open $tunnel_dir/$name for writing: $!"; print PEER "# # PPTP Tunnel configuration for tunnel $name # Server IP: $ip\n"; foreach my $r (@routes) { print PEER "# Route: $r\n"; } print PEER "# # # Tags for CHAP secret selection # name $local remotename $remote # # Include the main PPTP configuration file # file $pppdir/options.pptp "; close(PEER) or die "can't close $tunnel_dir/$name: $!"; print "Added tunnel $name\n"; } #DelTunnel <name> # # Deletes the tunnel named <name> # sub DelTunnel($) { my $name = $_[0]; return if(!defined $name || $name eq ""); if( ! -f "$tunnel_dir/$name" ) { print "ERROR! Peer $name does not exist!\n"; return; } # force $name to be untainted # ($name is clean because it passed the -f test above, and it's not # being sent to a shell. But -T doesn't know that.) $name =~ /^(.*)$/o; $name =$1; unlink "$tunnel_dir/$name"; print "Removed tunnel $name\n"; } #BreakSymlink <file> # # If <file> is a symlink # 1. break the link # 2. copy the contents of the file pointed to do <file> # sub BreakSymlink($) { my $file = shift; if( -l "$file" ) { my $link = readlink "$file"; $link = "$1/$link" if $file =~ m,(.*)/[^/], and not $link =~ m,^/,; print "Breaking symlink $file -> $link\n"; unlink "$file"; die "$file pointed at a strangely named file\n" if $link !~ /^($safe_re)$/; $link = $1; `cp $link $file`; } } #Rotate <target> <new> <old> # # Rotates config files. # # <target> - full path of the config file # <new> - full path of the file being rotated in # <old> - expected contents of the file being rotated out # # Example: # Rotate /etc/resolv.conf, /etc/resolv.conf.pptp, /etc/resolv.conf.real # sub Rotate($$$) { my ($target, $new, $old) = @_; return undef unless -f $new && -f $old; my $diff = `diff $target $new`; chomp $diff; return 1 if $diff eq ""; $diff = `diff $target $old`; chomp $diff; if($diff ne "") { print "WARNING: $new not installed\n"; print " $target does not match $old\n"; return undef; } `ln -sf $new $target`; print "Installed $new as $target\n"; return; } #AddCHAPorPAP - interactive # # Prompts for parameters and adds a CHAP or PAP secret # sub AddCHAPorPAP { my $secret_type = $_[0]; print "Add a NEW $secret_type secret. NOTE: Any backslashes (\\) must be doubled (\\\\). Local Name: This is the 'local' identifier for $secret_type authentication. NOTE: If the server is a Windows NT machine, the local name should be your Windows NT username including domain. For example: domain\\\\username "; my $local = QueryUser "Local Name", undef; print " Remote Name: This is the 'remote' identifier for $secret_type authentication. In most cases, this can be left as the default. It must be set if you have multiple $secret_type secrets with the same local name and different passwords. Just press ENTER to keep the default. "; my $remote = QueryUser "Remote Name", "PPTP"; print " Password: This is the password or $secret_type secret for the account specified. The password will not be echoed. "; # Get the password without echoing `stty -echo`; my $pass = QueryUser "Password", undef; `stty echo`; my $secrets_file = ""; if( $secret_type eq "CHAP") { $secrets_file = $chap_secrets; } elsif( $secret_type eq "PAP") { $secrets_file = $pap_secrets; } else { die ( "wrong sercet type!"); } open(SECRETS_FILE, ">>$secrets_file") or die ("couldn't open $secrets_file: $!"); print "\nAdding secret $local $remote *****\n\n"; print SECRETS_FILE "$local\t$remote\t$pass\n"; print SECRETS_FILE "$remote\t$local\t$pass\n"; close(SECRETS_FILE) or die ("couldn't close $secrets_file: $!"); chmod 0600, $secrets_file; } # /AddCHAPorPAP() #AddPPTP - interactive # # Add a new PPTP tunnel configuration # sub AddPPTP() { my ($name, $ip, $local, $remote); print "\nAdd a NEW PPTP Tunnel.\n\n"; my @configs = keys %pptp_servers; my $choice = bselect "Which configuration would you like to use?", @configs, "Other"; my @routes; if($choice == @configs+1) { while (1) { $name = QueryUser "Tunnel Name", undef; # per man perlsec, check for special characters if ($name =~ /^([-\@\w.]+)$/) { $name = $1; last; } print "Name contains special characters.\n"; print "Please use only alphanumerics, '-', '_', '.', and '\@'.\n"; } $ip = QueryUser "Server IP", undef; print "What route(s) would you like to add when the tunnel comes up?\n"; print "This is usually a route to your internal network behind the PPTP server.\n"; print "You can use TUNNEL_DEV and DEF_GW as in /etc/pptp.d/ config file\n"; print "TUNNEL_DEV is replaced by the device of the tunnel interface.\n"; print "DEF_GW is replaced by the existing default gateway.\n"; print "The syntax to use is the same as the route(8) command.\n"; print "Enter a blank line to stop.\n"; while (1) { my $route = QueryUser "route", undef; last unless defined $route; last if $route eq ""; if($route =~ /$unsafe_re/o) { print "$route contains unsafe characters. discarded.\n"; next; } push @routes, $route; } } else { $name = $configs[$choice-1]; $ip = $pptp_servers{$configs[$choice-1]}->{"ip"}; @routes = @{$pptp_servers{$configs[$choice-1]}->{"routes"}}; } print "Local Name and Remote Name should match a configured CHAP or PAP secret. Local Name is probably your NT domain\\username. NOTE: Any backslashes (\\) must be doubled (\\\\). "; $local = QueryUser "Local Name", undef; $remote = QueryUser "Remote Name", "PPTP"; print "Adding $name - $ip - $local - $remote\n"; AddTunnel $name, $ip, $local, $remote, @routes; } sub ConfigureResolv() { if(yesno "Use a PPTP-specific resolv.conf during tunnel connections?") { if( -f $resolv_pptp ) { print "$resolv_pptp exists.\n"; if(! yesno "Do you want to use the existing $resolv_pptp?") { print "Renaming $resolv_pptp --> $resolv_pptp.orig...\n"; rename $resolv_pptp, "$resolv_pptp.orig" or die "couldn't rename $resolv_pptp: $!"; } } if(! -f $resolv_pptp) { my @configs = keys %dns_servers; my $choice = bselect "Which configuration do you want to use?", @configs, "Other"; my (@addresses, $search); if($choice == @configs+1 ) { print "What domain names do you want to search for partially\n" . "specified names?\n"; print "Enter all of them on one line, seperated by spaces.\n"; $search = QueryUser "Domain Names", undef; print "Enter the IP addresses of your nameservers\n"; print "Enter a blank IP address to stop.\n"; while(1) { my $address = QueryUser "Nameserver IP Address", undef; last unless defined $address; last if $address eq ""; push @addresses, $address; } } else { $search = $dns_servers{$configs[$choice-1]}->{"search_list"}; @addresses = @{$dns_servers{$configs[$choice-1]}->{"ip_list"}}; } open(PPTP, ">$resolv_pptp") or die "couldn't open $resolv_pptp for writing: $!"; print PPTP "search $search\n"; foreach my $a (@addresses) { print PPTP "nameserver $a\n"; } close(PPTP) or die "couldn't close $resolv_pptp: $!"; } if( -f $resolv_real) { my $diff = `diff $resolv $resolv_real`; chomp $diff; if($diff ne "") { print "** $resolv_real exists.\n"; print "** copying it to $resolv_real.orig\n"; unlink "$resolv_real.orig"; rename $resolv_real, "$resolv_real.orig"; } } BreakSymlink $resolv; print "Copying $resolv to $resolv_real...\n"; `cp -f $resolv $resolv_real`; print "Creating link from $resolv_real to $resolv\n"; `ln -sf $resolv_real $resolv`; } else { #they choose not to twiddle /etc/resolv.conf BreakSymlink $resolv; if( -f $resolv_pptp) { print "$resolv_pptp exists\n"; if(yesno "Do you want to delete /etc/resolv.conf.pptp?") { unlink $resolv_pptp; print "$resolv_pptp deleted.\n"; } else { print "** You have chosen not to delete $resolv_pptp\n" . "** This existing $resolv_pptp may still be used\n" . "** when tunnel connections are established. If you\n" . "** really don't want it to be used, you should\n" . "** rename or remove it.\n"; } } if( -f $resolv_real) { my $diff = `diff $resolv $resolv.real`; chomp $diff; if($diff eq "") { print "$resolv is identical to $resolv_real\n"; if(yesno "Do you want to delete $resolv_real?") { unlink $resolv_real; print "$resolv_real deleted\n"; } } else { print "** $resolv and $resolv_real both exist\n" . "** but are not the same. You should decide which\n" . "** one is correct and make sure that file is named\n" . "** $resolv\n"; } } } } #getCHAPorPAP # # This returns all the CHAP or PAP secrets with ***ed out the paswords sub getCHAPorPAP { my $secret_type = $_[0]; my $secrets_file = ""; if( $secret_type eq "CHAP") { $secrets_file = $chap_secrets; } elsif( $secret_type eq "PAP") { $secrets_file = $pap_secrets; } else { die ( "wrong sercet type!"); } if(-f $secrets_file) { my @list= `cat $secrets_file`; foreach my $secret (@list) { $secret =~ s/(.*\s)\S+\s*$/$1*****\n/ unless $secret =~ /^\s*#/; } return @list; } else { return undef; } } #ManageSecrets # # This manages secret files sub ManageSecrets { my $secret_type=$_[0]; while(1) { my $manage_task = bselect "?", "List $secret_type secrets", "Add a New $secret_type secret", "Delete a $secret_type secret", "Quit"; if( $manage_task eq "1") { print "Current $secret_type secrets:\n"; my @list = getCHAPorPAP( $secret_type); if( @list ) { print @list; } else { print " None.\n"; } } elsif( $manage_task eq "2") { AddCHAPorPAP( $secret_type); } elsif( $manage_task eq "3") { my @list; my $secrets_file; if( $secret_type eq "CHAP") { $secrets_file = $chap_secrets; } elsif( $secret_type eq "PAP") { $secrets_file = $pap_secrets; } else { die "wrong secret_type!"; } @list = getCHAPorPAP( $secret_type); if( @list) { print "Select one of the pair of lines that you want removed.\n"; print "Both matching lines will be deleted.\n"; my $choice = bselect "Remove which $secret_type secret?", @list, "None"; $choice--; if($choice == @list) { print "Aborted Deleting a $secret_type secret\n"; next; } else { `stty -echo`; my $passwd = QueryUser "Enter the password for this $secret_type secret", undef; `stty echo`; my @secrets = `cat $secrets_file`; open(SECRETS_FILE, ">$secrets_file") or die "Couldn't open $secrets_file for writing: $!"; my ($local, $remote, undef) = split(/\s/, $list[$choice]); my $count = 0; foreach my $c (@secrets) { my ($c_local, $c_remote, $c_secret, undef) = split(/\s/, $c); if( $c_secret eq $passwd && ( ($c_local eq $local && $c_remote eq $remote) || ($c_local eq $remote && $c_remote eq $local) )) { $count++; next; } else { print SECRETS_FILE $c; } } close(SECRETS_FILE) or die "Couldn't close $secrets_file after writing: $!"; print "\nDeleted $count entries."; print " Perhaps you mistyped the password?" if $count == 0; print "\n"; } } } elsif( $manage_task eq "4" || $manage_task eq "q") { last; } else { next; } } } #setup # # This is the part that does the old pptp-setup work. #first the site-specific config files sub setup() { my ($name, $search_list, $ip_list, $ip, @configs); foreach my $f (`ls $pptpdir`) { if($f !~ /^($safe_re)$/o) { print "Name your files something reasonable: \"$f\" doesn't qualify\n"; next; } $f = $1; open(CONFIG, "<$pptpdir/$f") or next; #silently fail here @configs = <CONFIG>; close CONFIG; chomp $f; for(my $i=0; $i<=$#configs; $i++) { $configs[$i] =~ s/\#.*/ /o; if($configs[$i] =~ /\S/) { chomp $configs[$i]; if($configs[$i] eq "nameservers") { until(++$i == @configs) { ($name,$search_list,$ip_list) = split ':', $configs[$i]; $name = $f ."-". $name; $dns_servers{$name}->{"search_list"}=$search_list; $dns_servers{$name}->{"ip_list"}=[split ' ', $ip_list]; } } else { ($name,$ip) = split ' ', $configs[$i]; $name = $f ."-". $name; $pptp_servers{$name}->{"ip"}=$ip; $pptp_servers{$name}->{"routes"}=[]; until($configs[++$i] eq "\n") { chomp $configs[$i]; if($configs[$i] =~ /$unsafe_re/o ) { print "WARNING: the line:\n", "$configs[$i]\n", "contains unsafe characters!\n"; next; } $pptp_servers{$name}->{"routes"}=[@{$pptp_servers{$name}->{"routes"}},$configs[$i]]; } } } } } #ok. now all the info from the config files is in %pptp_servers and %dns_servers. now let's do something with it. while(1) { my $task = bselect "?", "Manage CHAP secrets", "Manage PAP secrets", "List PPTP Tunnels", "Add a NEW PPTP Tunnel", "Delete a PPTP Tunnel", "Configure resolv.conf", "Select a default tunnel", "Quit"; if($task eq "1") { ManageSecrets( "CHAP"); } elsif($task eq "2") { ManageSecrets( "PAP"); } elsif($task eq "3") { my @tunnels = ConfiguredTunnels; print "Current Tunnels:\n"; if(scalar(@tunnels) != 0) { print join "\n", @tunnels; print "\n"; } else { print " None.\n"; } } elsif($task eq "4") { AddPPTP; } elsif($task eq "5") { my $tunnel = SelectTunnel "Delete which tunnel?"; DelTunnel $tunnel if $tunnel ne ""; } elsif($task eq "6") { ConfigureResolv; } elsif($task eq "7") { my @tunnels = ConfiguredTunnels; if( -l "$tunnel_dir/__default" ) { print "The current default is ".readlink("$tunnel_dir/__default")."\n"; } if( -f _ ) { die "$tunnel_dir/__default is a regular file not a symlink!\n"; } my $choice = bselect "Which tunnel do you want to be the default?", @tunnels, "cancel"; next if $choice == @tunnels+1; unlink "$tunnel_dir/__default"; my $scratch = $tunnel_dir."/".$tunnels[$choice-1]; $scratch = $1 if $scratch =~ /^($safe_re)$/o; symlink $scratch, "$tunnel_dir/__default" or die "couldn't create __defualt symlink: $!"; } elsif($task eq "8" || $task eq "q") { exit 0; } } } #start # # This does the old pptp-start work sub start() { my ($tunnel, $f, @filter, @ifs, $if, @foo); my @tunnels = ConfiguredTunnels; die "no configured tunnels!\n" if @tunnels == 0; if(defined $ARGV[1]) { $tunnel = $ARGV[1]; } elsif(-l "$tunnel_dir/__default" && defined $ARGV[0]) { my $default = readlink "$tunnel_dir/__default"; $tunnel = (split '/', $default)[-1]; } elsif(-t STDIN && -t STDOUT) { $tunnel = SelectTunnel "Start a tunnel to which server?"; } else { usage; } die "Nasty characters in $tunnel\n" if $tunnel !~ /^($safe_re)$/o; $tunnel = $1; my $config = "$tunnel_dir/$tunnel"; die "Tunnel configuration for $tunnel not found\n" unless -f $config; open(CONFIG, "<$config") or die "couldn't open $config: $!"; my @conf = <CONFIG>; close CONFIG; my ($ip,undef) = grep {/Server IP/} @conf; my $server = undef; $server = $1 if $ip =~ /.*IP: ([-a-zA-Z0-9\.]+).*/; die "Server Address for $tunnel not found.\n" unless defined $server; #build a regexp of the currently existing interfaces my @ifconfig = `/sbin/ifconfig`; foreach $f (@ifconfig) { next unless $f =~ /^[a-z]/; @foo=split ' ', $f; push @filter, $foo[0]; } my $if_re = join '|', @filter; #bring up the tunnel my $child = fork; if ($child == 0) { exec "/usr/sbin/pptp $server call $tunnel"; die "exec of pptp failed."; } my $timeout=60; while(1) { die "ERROR! Connection timed out.\n" if $timeout==0; $timeout--; @ifs = (); sleep 1; @ifconfig=`/sbin/ifconfig`; foreach $f (@ifconfig) { next unless $f =~ /^[a-z]/; @foo=split ' ', $f; push @ifs, $foo[0]; } ($if, undef) = grep {!/$if_re/} @ifs; last if defined $if; } die "something screwy in your interface names: $if\n" if $if !~ /^($safe_re)$/o; $if = $1; (grep {/inet/} `/sbin/ifconfig $if`)[0] =~ /:(\d+\.\d+\.\d+\.\d+)/; $ip = $1; my (undef, $gw, undef) = split ' ', (`/sbin/route -n`)[-1]; my @routes = grep {/Route/} @conf; open(LOCK, ">>$subsys_dir/pptp") or die "couldn't open lock file: $!"; foreach my $r (@routes) { chomp $r; $r =~ s/.*?Route: //; if ($r !~ /^($safe_re)$/o) { print "WARNING: $r countains unsafe characters. Ignoring it.\n"; next; } $r = $1; $r =~ s/TUNNEL_DEV/$if/og; $r =~ s/DEF_GW/$gw/og; # script runs in tainted mode, so $r has to be detaineted/laundered # (funny thing is, it should be clean already ...) # $r should be safe (see above: $safe_re) $r =~ m/(.*)/; $r = $1; die "route failed on $r" if system("/sbin/route $r"); #store the routes added in the lock file so they can be ripped down during stop. print "Route: $r added\n"; print LOCK "$r\n"; } close LOCK or die "couldn't close lock file: $!"; print "All routes added.\n"; print "Tunnel $tunnel is active on $if. IP Address: $ip\n"; Rotate $resolv, $resolv_pptp, $resolv_real; exit 0; } #stop # # this does the old pptp-stop work sub stop() { Rotate $resolv, $resolv_real, $resolv_pptp; print "Sending HUP signal to PPPD processes...\n"; # (sb) kill pppd instead of pptp - cleaner interaction with "service" `killall -HUP pppd`; open(LOCK, "<$subsys_dir/pptp") or goto "skip"; while(my $r = <LOCK>) { chomp $r; if ($r !~ /^($safe_re)$/o) { print "someone is messing with the lock files in a bad way\n"; print "ignoring all remaining route commands.\n"; last; } $r = $1; $r =~ s/add/del/o; system("/sbin/route $r >/dev/null 2>&1"); #many of these will fail... that's fine. } close LOCK; skip: unlink "$subsys_dir/pptp"; sleep 2; exit 0; } if(defined $ARGV[0]) { if($ARGV[0] eq "setup") { setup; } elsif($ARGV[0] eq "start") { start; } elsif($ARGV[0] eq "stop") { stop; } elsif($ARGV[0] eq "status") { if( -f "$subsys_dir/pptp") { print "There is probably a pptp tunnel up\n"; exit 0; } else { print "There is probably not a pptp tunnel up\n"; exit 3; } } elsif($ARGV[0] eq "restart" || $ARGV[0] eq "force-reload" || $ARGV[0] eq "reload") { print STDERR "$ARGV[0] is not implimented yet\n"; exit 3; } } if(! -t STDIN || ! -t STDOUT) { usage; } my $mode = bselect "What task would you like to do?", "start", "stop", "setup", "quit"; if($mode eq "1") { start; } elsif($mode eq "2") { stop; } elsif($mode eq "3") { setup; } elsif($mode eq "4" or $mode eq "q") { exit 0; }
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor