diff --git a/logs.go b/logs.go new file mode 100644 index 0000000..a1e58dc --- /dev/null +++ b/logs.go @@ -0,0 +1,11 @@ +package main + +import ( + "log" +) + +func (c *vpnSession) Log() error { + //TODO get asname & shit + log.Println(c) + return nil +} diff --git a/otp.go b/otp.go index be36212..f269e62 100644 --- a/otp.go +++ b/otp.go @@ -13,6 +13,14 @@ func (s *OpenVpnMgt) GenerateOTP(user string) ([]string, error) { // return s.GenerateOTPGeneric(user, 60, "sha256", 30, 8) // } +func (s *OpenVpnMgt) TokenPassword(c *vpnSession) (bool, string) { + //TODO implement that correcly + if c.password == "maith1wiePuw3ieb4heiNie5y" { + return true, "maith1wiePuw3ieb4heiNie5y" + } + return false, "maith1wiePuw3ieb4heiNie5y" +} + func (s *OpenVpnMgt) GenerateOTPGeneric(user string, period int, algo string, secretLen int, digits int) ([]string, error) { codes := []string{} now := time.Now() diff --git a/vpnserver.go b/vpnserver.go index 9bb7f5f..0301b9f 100644 --- a/vpnserver.go +++ b/vpnserver.go @@ -3,11 +3,12 @@ package main import ( "bufio" "errors" - "fmt" "io" "log" "net" "os" + "regexp" + "strconv" "strings" "sync" ) @@ -15,11 +16,11 @@ import ( // Server represents the server type OpenVpnMgt struct { Port string - buf *bufio.ReadWriter - connected bool + 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 @@ -35,9 +36,11 @@ type OpenVpnMgt struct { // 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), + Port: port, + ret: make(chan []string), + ldap: make(map[string]ldapConfig), + buf: make(map[string]*bufio.ReadWriter), + clients: make(map[string]map[int]*vpnSession), } } @@ -68,129 +71,18 @@ func (s *OpenVpnMgt) Run() { } } -func (s *OpenVpnMgt) TokenPassword(c *vpnSession) (bool, string) { - //TODO implement that correcly - if c.password == "maith1wiePuw3ieb4heiNie5y" { - return true, "maith1wiePuw3ieb4heiNie5y" - } - return false, "maith1wiePuw3ieb4heiNie5y" -} - -// main authentication function. -// returns 0 if auth is valid -// returns 1 if an TOTP code is necessary -// returns a negative if auth is not valid -func (s *OpenVpnMgt) Auth(c *vpnSession) (error, int) { - // an empty password is not good - if c.password == "" { - return errors.New("Empty Password"), -1 - } - - // check if the password is a valid token (see TOTP request) - tokenPasswordOk, tokenPassword := s.TokenPassword(c) - - // password is a token. We remove it from the session object to - // avoid checking it against the ldap - if tokenPasswordOk { - c.password = "" - } - - // if the otp is not empty, we check it against the valid codes as soon as - // possible - otpvalidated := false - if c.otpCode != "" { - codes, err := s.GenerateOTP(c.Login) - if err != nil { - return err, -2 - } - for _, possible := range codes { - if possible == c.otpCode { - otpvalidated = true - } - } - } - - c.Profile = "" - login := []string{c.Login} - pass := c.password - - for { - n := c.Profile - for k, ldap := range s.ldap { - if ldap.upgradeFrom != c.Profile { - continue - } - err, userOk, passOk, secondary := ldap.Auth(login, pass) - - // if there is an error, try the other configurations - if err != nil { - log.Println(err) - continue - } - - // we did find a valid User - if userOk { - // the login for the new auth level is given by the current one - login = secondary - - if c.Mail == "" { - c.Mail = secondary[0] - } - - if passOk && c.Profile != "" { - // it's at least the second auth level, and we have a valid - // password on 2 different auth system. It's a dupplicate - // password, let's log it - log.Printf("User %s has a dupplicate password\n", c.Login) - } - - // we have either a positive auth ok a previous valid one - if passOk || c.Profile != "" || tokenPasswordOk { - c.Profile = k - } - } - } - - // no profile update this turn, no need to continue - if n == c.Profile { - break - } - } - - // no profile validated, we stop here - if c.Profile == "" { - return errors.New("Authentication Failed"), -3 - } - - // check the MFA requested by the secured profile - switch s.ldap[c.Profile].mfaType { - case "internal": - if otpvalidated { - return nil, 0 - } - c.password = tokenPassword - return errors.New("Need OTP Code"), 1 - case "okta": - //TODO implement okta MFA - return nil, -4 - } - - // no MFA requested, the login is valid - return nil, 0 -} - -func (s *OpenVpnMgt) sendCommand(msg []string) (error, []string) { - if !s.connected { +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 { log.Println(line) - if _, err := s.buf.WriteString(line + "\r\n"); err != nil { + if _, err := s.buf[remote].WriteString(line + "\r\n"); err != nil { return err, nil } } - if err := s.buf.Flush(); err != nil { + if err := s.buf[remote].Flush(); err != nil { return err, nil } @@ -199,30 +91,72 @@ func (s *OpenVpnMgt) sendCommand(msg []string) (error, []string) { return nil, ret } -func (s *OpenVpnMgt) Help() (error, []string) { - err, msg := s.sendCommand([]string{"help"}) - if err != nil { - return err, nil +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, msg + return nil, ret } -func (s *OpenVpnMgt) Version() (error, []string) { - err, msg := s.sendCommand([]string{"version"}) - if err != nil { - return err, nil +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, msg + return nil, ret } -func (s *OpenVpnMgt) ClientValidated(line string) { - //TODO manage that : find the client, log stuff +func (s *OpenVpnMgt) ClientValidated(line, remote string) { + err, c := s.getClient(line, remote) + if err != nil { + log.Println(err, line) + return + } <-s.ret + + c.Status = "success" + + log.Println(c) } -func (s *OpenVpnMgt) ClientDisconnect(line string) { - //TODO manage that : find the client, log stuff +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 + if c.Status != "Need OTP Code" { + c.Log() + } + + defer delete(s.clients[remote], c.cID) } func (s *OpenVpnMgt) getIP(c *vpnSession) (string, error) { @@ -232,124 +166,141 @@ func (s *OpenVpnMgt) getIP(c *vpnSession) (string, error) { return ip.String(), nil } -func (s *OpenVpnMgt) ClientConnect(line string) { - var cmd []string - var ip string - var errIP error - +func (s *OpenVpnMgt) ClientConnect(line, remote string) { client := NewVPNSession("log in") + client.vpnserver = remote client.ParseSessionId(line) + s.clients[remote][client.cID] = client infos := <-s.ret if err := client.ParseEnv(&infos); err != nil { log.Println(err) return } - err, ok := s.Auth(client) + client.Auth(s) +} - // if auth is ok, time to get an IP address - if ok == 0 { - ip, errIP = s.getIP(client) - if errIP != nil { - ok = -10 - err = errIP - } +// 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 } - - switch { - case ok == 0: - cmd = []string{ - fmt.Sprintf("client-auth %d %d", client.cID, client.kID), - fmt.Sprintf("ifconfig-push %s %s", ip, client.localIP), - } - for _, r := range s.ldap[client.Profile].routes { - cmd = append(cmd, fmt.Sprintf("push \"route %s vpn_gateway\"", r)) - } - cmd = append(cmd, "END") - - case ok < 0: - cmd = []string{fmt.Sprintf("client-deny %d %d \"%s\" \"%s\"", - client.cID, client.kID, err, err)} - - case ok == 1: - cmd = []string{fmt.Sprintf( - "client-deny %d %d \"Need OTP\" \"CRV1:R,E:%s:%s:OTP Code \"", - client.cID, client.kID, client.password, client.b64Login())} + id, err := strconv.Atoi(match[1]) + if err != nil { + return err, nil } - - if err, _ := s.sendCommand(cmd); err != nil { - log.Println(err) + if _, ok := s.clients[remote]; !ok { + return errors.New("unknown vpn server"), nil } - - return + if c, ok := s.clients[remote][id]; ok { + return nil, c + } + return errors.New("unknown vpn client"), nil } func (s *OpenVpnMgt) handleConn(conn net.Conn) { - defer conn.Close() + remote := conn.RemoteAddr().String() - // we don't want multiple connexions, only one openvpn server at a time - s.m.Lock() - if s.connected { - conn.Write([]byte("Sorry, only one server allowed\n")) - s.m.Unlock() - return - } - s.connected = true - s.m.Unlock() + 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 = bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn)) + 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 10\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.Println("Valid openvpn connected from %s", remote) + for { - line, err := s.buf.ReadString('\n') + line, err := s.buf[remote].ReadString('\n') // manage basic errors switch { case err == io.EOF: log.Println("Reached EOF - close this connection.\n") - s.connected = false return case err != nil: log.Println("Error reading line. Got: '"+line+"'\n", err) - s.connected = false 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"} { if strings.HasPrefix(line, terminator) { s.ret <- response response = nil + line = "" break } } switch { - // a new openvpn server is connected - case strings.HasPrefix(line, ">INFO"): - // command sucessfull, we can ignore + // command successfull, we can ignore case strings.HasPrefix(line, ">SUCCESS: client-deny command succeeded"): + // trafic stats + case strings.HasPrefix(line, ">BYTECOUNT_CLI"): + //TODO use that + // 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) + 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"): - go s.ClientValidated(line) + go s.ClientValidated(line, remote) case strings.HasPrefix(line, ">CLIENT:CONNECT"): - go s.ClientConnect(line) + go s.ClientConnect(line, remote) default: response = append(response, line) } - log.Print(line) + // TODO remove this + if strings.Index(line, "password") == -1 { + log.Print(line) + } } } diff --git a/vpnsession.go b/vpnsession.go index f0e497e..a301b8b 100644 --- a/vpnsession.go +++ b/vpnsession.go @@ -3,6 +3,9 @@ package main import ( "encoding/base64" "encoding/json" + "errors" + "fmt" + "log" "os" "regexp" "strconv" @@ -33,6 +36,7 @@ type vpnSession struct { password string `json:"-"` otpCode string `json:"-"` localIP string `json:"-"` + vpnserver string `json:"-"` } func NewVPNSession(operation string) *vpnSession { @@ -46,6 +50,13 @@ func NewVPNSession(operation string) *vpnSession { return &v } +func (c *vpnSession) String() string { + if res, err := json.MarshalIndent(c, " ", " "); err == nil { + return string(res) + } + return "" +} + func (c *vpnSession) b64Login() string { return base64.StdEncoding.EncodeToString([]byte(c.Login)) } @@ -92,6 +103,9 @@ func (c *vpnSession) ParseEnv(infos *[]string) error { } c.password = split[2] c.otpCode = split[4] + if c.otpCode == "" { + c.otpCode = "***" + } case strings.HasPrefix(p[1], "SCRV1"): split := strings.Split(p[1], ":") @@ -106,13 +120,18 @@ func (c *vpnSession) ParseEnv(infos *[]string) error { data, err = base64.StdEncoding.DecodeString(split[2]) if err != nil { + c.password = p[1] break } c.otpCode = string(data) + if c.otpCode == "" { + c.otpCode = "***" + } + default: c.password = p[1] - c.otpCode = "***" + c.otpCode = "" } case "username": @@ -124,9 +143,160 @@ func (c *vpnSession) ParseEnv(infos *[]string) error { return nil } -func (c *vpnSession) String() string { - if res, err := json.MarshalIndent(c, " ", " "); err == nil { - return string(res) +func (c *vpnSession) Auth(s *OpenVpnMgt) { + var cmd []string + var ip string + var errIP error + + err, ok := c.auth(s) + // if auth is ok, time to get an IP address + if ok == 0 { + ip, errIP = s.getIP(c) + if errIP != nil { + ok = -10 + err = errIP + } } - return "" + + switch { + case ok == 0: + cmd = []string{ + fmt.Sprintf("client-auth %d %d", c.cID, c.kID), + fmt.Sprintf("ifconfig-push %s %s", ip, c.localIP), + } + for _, r := range s.ldap[c.Profile].routes { + cmd = append(cmd, fmt.Sprintf("push \"route %s vpn_gateway\"", r)) + } + cmd = append(cmd, "END") + + case ok < 0: + cmd = []string{fmt.Sprintf("client-deny %d %d \"%s\" \"%s\"", + c.cID, c.kID, err, err)} + + case ok == 1: + cmd = []string{fmt.Sprintf( + "client-deny %d %d \"Need OTP\" \"CRV1:R,E:%s:%s:OTP Code \"", + c.cID, c.kID, c.password, c.b64Login())} + } + + if err, _ := s.sendCommand(cmd, c.vpnserver); err != nil { + log.Println(err) + } + + return +} + +// main authentication function. +// returns 0 if auth is valid +// returns 1 if an TOTP code is necessary +// returns a negative if auth is not valid +func (c *vpnSession) auth(s *OpenVpnMgt) (error, int) { + // an empty password is not good + if c.password == "" { + c.Status = "Empty Password" + return errors.New("Empty Password"), -1 + } + + // check if the password is a valid token (see TOTP request) + tokenPasswordOk, tokenPassword := s.TokenPassword(c) + + // password is a token. We remove it from the session object to + // avoid checking it against the ldap + if tokenPasswordOk { + c.password = "" + } + + // if the otp is not empty, we check it against the valid codes as soon as + // possible + otpvalidated := false + if c.otpCode != "" { + codes, err := s.GenerateOTP(c.Login) + if err != nil { + return err, -2 + } + for _, possible := range codes { + if possible == c.otpCode { + otpvalidated = true + } + } + } + + c.Profile = "" + login := []string{c.Login} + pass := c.password + + for { + n := c.Profile + for k, ldap := range s.ldap { + if ldap.upgradeFrom != c.Profile { + continue + } + err, userOk, passOk, secondary := ldap.Auth(login, pass) + + // if there is an error, try the other configurations + if err != nil { + log.Println(err) + continue + } + + // we did find a valid User + if userOk { + // the login for the new auth level is given by the current one + login = secondary + + if c.Mail == "" { + c.Mail = secondary[0] + } + + if passOk && c.Profile != "" { + // it's at least the second auth level, and we have a valid + // password on 2 different auth system. It's a dupplicate + // password, let's log it + log.Printf("User %s has a dupplicate password\n", c.Login) + } + + // we have either a positive auth ok a previous valid one + if passOk || c.Profile != "" || tokenPasswordOk { + c.Profile = k + } + } + } + + // no profile update this turn, no need to continue + if n == c.Profile { + break + } + } + + // no profile validated, we stop here + if c.Profile == "" { + c.Status = "fail (password)" + return errors.New("Authentication Failed"), -3 + } + + // check the MFA requested by the secured profile + c.TwoFA = true + switch s.ldap[c.Profile].mfaType { + case "internal": + if otpvalidated { + return nil, 0 + } + // log that the failure is due to the OTP + if c.otpCode == "" { + c.Status = "Need OTP Code" + } else { + c.Status = "fail (OTP) : " + } + c.password = tokenPassword + return errors.New("Need OTP Code"), 1 + case "okta": + //TODO implement okta MFA + c.Status = "fail (Okta)" + return nil, -4 + default: + c.TwoFA = false + } + + // no MFA requested, the login is valid + return nil, 0 }