package main import ( "bufio" "errors" "fmt" "io" "log" "net" "os" "regexp" "strconv" "strings" "sync" hibp "github.com/mattevans/pwned-passwords" ) // Server represents the server type OpenVpnMgt struct { port string buf map[string]*bufio.ReadWriter m sync.RWMutex ret chan []string ldap map[string]ldapConfig clients map[string]map[int]*vpnSession authCa string vpnlogUrl string mailRelay string MailFrom string CcPwnPassword string pwnTemplate string newAsTemplate string cacheDir string syslog bool ipRouteScript string otpMasterSecrets []string hibpClient *hibp.Client debug bool } // NewServer returns a pointer to a new server func NewVPNServer(port string) *OpenVpnMgt { return &OpenVpnMgt{ port: port, ret: make(chan []string), ldap: make(map[string]ldapConfig), buf: make(map[string]*bufio.ReadWriter), clients: make(map[string]map[int]*vpnSession), hibpClient: hibp.NewClient(), } } // Run starts a the server func (s *OpenVpnMgt) Run() { // Resolve the passed port into an address addrs, err := net.ResolveTCPAddr("tcp", s.port) if err != nil { log.Println(err) os.Exit(1) } // start listening to client connections listener, err := net.ListenTCP("tcp", addrs) if err != nil { log.Println(err) os.Exit(1) } // Infinite loop since we dont want the server to shut down for { // Accept the incomming connections conn, err := listener.Accept() if err != nil { // continue accepting connection even if an error occurs (if error occurs dont shut down) continue } // run it as a go routine to allow multiple clients to connect at the same time go s.handleConn(conn) } } func (s *OpenVpnMgt) CheckPwn(c *vpnSession) error { c.LogPrintln("checking pwn password") pwned, err := s.hibpClient.Pwned.Compromised(c.password) if err != nil { return err } c.PwnedPasswd = pwned return nil } // send a command to the server. Set the channel to receive the response func (s *OpenVpnMgt) sendCommand(msg []string, remote string) (error, []string) { if len(s.buf) == 0 { return errors.New("No openvpn server present"), nil } for _, line := range msg { if s.debug { log.Println(line) } if _, err := s.buf[remote].WriteString(line + "\r\n"); err != nil { return err, nil } } if err := s.buf[remote].Flush(); err != nil { return err, nil } // wait for the response ret := <-s.ret if s.debug { for _, line := range ret { log.Println(line) } } return nil, ret } // send the list of all connected clients func (s *OpenVpnMgt) Stats() map[string]map[int]*vpnSession { return s.clients } func (s *OpenVpnMgt) Kill(session string, id int) error { if _, ok := s.clients[session]; !ok { return errors.New("unknown session") } if _, ok := s.clients[session][id]; !ok { return errors.New("unknown session id") } err, msg := s.sendCommand([]string{fmt.Sprintf("client-kill %d", id)}, session) return err } // send the help command on all vpn servers. Kind of useless func (s *OpenVpnMgt) Help() (error, map[string]map[string]string) { ret := make(map[string]map[string]string) re := regexp.MustCompile("^(.*[^ ]) *: (.*)$") for remote := range s.buf { help := make(map[string]string) err, msg := s.sendCommand([]string{"help"}, remote) if err != nil { return err, ret } for _, line := range msg { match := re.FindStringSubmatch(line) if len(match) == 0 { continue } help[match[1]] = match[2] } ret[remote] = help } return nil, ret } // send the version command on all vpn servers. Kind of useless func (s *OpenVpnMgt) Version() (error, map[string][]string) { ret := make(map[string][]string) for remote := range s.buf { err, msg := s.sendCommand([]string{"version"}, remote) if err != nil { return err, ret } ret[remote] = msg } return nil, ret } // called after a client is confirmed connected and authenticated func (s *OpenVpnMgt) ClientValidated(line, remote string) { err, c := s.getClient(line, remote) if err != nil { log.Println(err, line) return } c.Status = "success" infos := <-s.ret if err := c.ParseEnv(s, &infos); err != nil { log.Println(err) } s.Log(c) } // called after a client is disconnected, including for auth issues func (s *OpenVpnMgt) ClientDisconnect(line, remote string) { err, c := s.getClient(line, remote) if err != nil { log.Println(err) return } <-s.ret // if the disconnect is due to an auth failure, don't change the status if c.Status == "success" { c.Operation = "log out" } // Don't log the initial auth failure due to absence of OTP code // And don't log the auth failure during re auth if c.Operation != "re auth" && c.Status != "Need OTP Code" { s.Log(c) } defer delete(s.clients[remote], c.cID) } // called at the initial connexion func (s *OpenVpnMgt) ClientConnect(line, remote string) { c := NewVPNSession() c.vpnserver = remote c.ParseSessionId(line) s.clients[remote][c.cID] = c infos := <-s.ret if err := c.ParseEnv(s, &infos); err != nil { log.Println(err) return } c.Auth(s) } func (s *OpenVpnMgt) ClientReAuth(line, remote string) { err, c := s.getClient(line, remote) if err != nil { log.Println(err, line) return } c.ParseSessionId(line) infos := <-s.ret if err := c.ParseEnv(s, &infos); err != nil { log.Println(err) return } // reset some values c.Profile = "" c.Status = "system failure" c.Operation = "re auth" c.Auth(s) } // find a client among all registered sessions func (s *OpenVpnMgt) getClient(line, remote string) (error, *vpnSession) { re := regexp.MustCompile("^[^0-9]*,([0-9]+)[^0-9]*") match := re.FindStringSubmatch(line) if len(match) == 0 { return errors.New("invalid message"), nil } id, err := strconv.Atoi(match[1]) if err != nil { return err, nil } if _, ok := s.clients[remote]; !ok { return errors.New("unknown vpn server"), nil } if c, ok := s.clients[remote][id]; ok { return nil, c } return errors.New("unknown vpn client"), nil } // update counters func (s *OpenVpnMgt) updateCounters(line, remote string) { p := strings.Split(strings.Replace(line, ":", ",", 1), ",") err, c := s.getClient(p[0]+","+p[1], remote) if err != nil { log.Println(err, line) return } if c.BwWrite, err = strconv.Atoi(p[2]); err != nil { c.LogPrintln(err) return } if c.BwRead, err = strconv.Atoi(p[3]); err != nil { c.LogPrintln(err) return } return } // main loop for a given openvpn server func (s *OpenVpnMgt) handleConn(conn net.Conn) { remote := conn.RemoteAddr().String() defer conn.Close() defer delete(s.buf, remote) defer delete(s.clients, remote) // we store the buffer pointer in the struct, to be accessed from other methods s.buf[remote] = bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn)) s.clients[remote] = make(map[int]*vpnSession) // most response are multilined, use response to concatenate them response := []string{} // remove bogus clients line, err := s.buf[remote].ReadString('\n') if err != nil { log.Println(err) return } if line != ">INFO:OpenVPN Management Interface Version 1 -- type 'help' for more info\r\n" { log.Println("Bogus Client") return } // ask for statistics if _, err := s.buf[remote].WriteString("bytecount 30\r\n"); err != nil { log.Println(err) return } if err := s.buf[remote].Flush(); err != nil { log.Println(err) return } if line, err := s.buf[remote].ReadString('\n'); err != nil || line != "SUCCESS: bytecount interval changed\r\n" { log.Println("Bogus Client") return } log.Printf("Valid openvpn connected from %s\n", remote) for { line, err := s.buf[remote].ReadString('\n') // manage basic errors switch { case err == io.EOF: log.Println("Reached EOF - close this connection.\n") return case err != nil: log.Println("Error reading line. Got: '"+line+"'\n", err) return } line = strings.Trim(line, "\n\r ") // manage exit commands for _, terminator := range []string{"quit", "exit"} { if line == terminator || strings.HasPrefix(line, terminator+" ") { log.Println("server disconnected") return } } // manage all "terminator" lines for _, terminator := range []string{"END", ">CLIENT:ENV,END", "SUCCESS", "ERROR"} { if strings.HasPrefix(line, terminator) { s.ret <- response response = nil line = "" break } } switch { // command successfull, we can ignore case strings.HasPrefix(line, ">SUCCESS: client-deny command succeeded"): // trafic stats case strings.HasPrefix(line, ">BYTECOUNT_CLI"): go s.updateCounters(line, remote) // new bloc for a disconnect event. // We start the receiving handler, which will wait for the Channel message case strings.HasPrefix(line, ">CLIENT:DISCONNECT"): go s.ClientDisconnect(line, remote) // new bloc for a connect event. // We start the receiving handler, which will wait for the Channel message case strings.HasPrefix(line, ">CLIENT:ADDRESS"): case strings.HasPrefix(line, ">CLIENT:ESTABLISHED"): go s.ClientValidated(line, remote) case strings.HasPrefix(line, ">CLIENT:CONNECT"): go s.ClientConnect(line, remote) case strings.HasPrefix(line, ">CLIENT:REAUTH"): go s.ClientReAuth(line, remote) default: response = append(response, line) } if s.debug && strings.Index(line, "password") == -1 { log.Print(line) } } }