package main import ( "encoding/base64" "encoding/json" "errors" "fmt" "github.com/pyke369/golang-support/rcache" "os" "os/exec" "strconv" "strings" "time" ) type vpnSession struct { Time time.Time `json:"time"` Login string `json:"username"` Operation string `json:"operation"` Status string `json:"status"` Profile string `json:"profile"` TwoFA bool `json:"2fa_auth"` IP string `json:"client_ip"` PrivIP string `json:"private_ip"` AsNumber string `json:"as_number"` AsName string `json:"as_name"` NewAS bool `json:"as_new"` PwnedPasswd bool `json:"pwned_passwd"` Hostname string `json:"hostname"` TooMuchPwn bool `json:"too_much_pwn"` BwRead int `json:"in_bytes"` BwWrite int `json:"out_bytes"` Mail string `json:"-"` cID int `json:"-"` kID int `json:"-"` port int `json:"-"` dev string `json:"-"` netmask string `json:"-"` password string `json:"-"` otpCode string `json:"-"` localIP string `json:"-"` vpnserver string `json:"-"` pwnMail string `json:"-"` newAsMail string `json:"-"` MailFrom string `json:"-"` CcPwnPassword string `json:"-"` } func NewVPNSession() *vpnSession { v := vpnSession{ Time: time.Now().Round(time.Second), Status: "system failure", Operation: "log in", } v.Hostname, _ = os.Hostname() 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)) } func (c *vpnSession) baseHash(salt string, i int64) string { return fmt.Sprintf("%s%s%s%s", salt, c.Login, c.IP, i) } func (c *vpnSession) AddRoute(ip string) error { var cmd *exec.Cmd if os.Geteuid() == 0 { cmd = exec.Command("/bin/ip", "route", "replace", ip+"/32", "dev", c.dev) } else { cmd = exec.Command("/usr/bin/sudo", "/bin/ip", "route", "replace", ip+"/32", "dev", c.dev) } return cmd.Run() } func (c *vpnSession) ParseSessionId(line string) error { var err error re := rcache.Get("^>CLIENT:[^,]*,([0-9]+),([0-9]+)$") match := re.FindStringSubmatch(line) if len(match) == 0 { return errors.New("invalid message") } if c.cID, err = strconv.Atoi(match[1]); err != nil { return err } if c.kID, err = strconv.Atoi(match[2]); err != nil { return err } return nil } func (c *vpnSession) ParseEnv(s *OpenVpnMgt, infos *[]string) error { var err error r := rcache.Get("[^a-zA-Z0-9./_@-]") renv := rcache.Get("^>CLIENT:ENV,([^=]*)=(.*)$") for _, line := range *infos { p := renv.FindStringSubmatch(line) if len(p) != 3 { continue } switch p[1] { case "trusted_port": if c.port, err = strconv.Atoi(r.ReplaceAllString(p[2], "")); err != nil { return err } case "untrusted_port": if c.port, err = strconv.Atoi(r.ReplaceAllString(p[2], "")); err != nil { return err } case "trusted_ip": c.IP = r.ReplaceAllString(p[2], "") case "untrusted_ip": c.IP = r.ReplaceAllString(p[2], "") case "ifconfig_pool_remote_ip": c.PrivIP = r.ReplaceAllString(p[2], "") case "ifconfig_local": c.localIP = r.ReplaceAllString(p[2], "") case "bytes_received": if c.BwWrite, err = strconv.Atoi(p[2]); err != nil { break } case "bytes_sent": if c.BwRead, err = strconv.Atoi(p[2]); err != nil { break } case "password": switch { case strings.HasPrefix(p[2], "CRV1"): split := strings.Split(p[2], ":") if len(split) != 5 { break } c.password = split[2] c.otpCode = split[4] if c.otpCode == "" { c.otpCode = "***" } // don't check that password against the ibp database case strings.HasPrefix(p[2], "SCRV1"): split := strings.Split(p[2], ":") if len(split) != 3 { break } data, err := base64.StdEncoding.DecodeString(split[1]) if err != nil { break } c.password = string(data) data, err = base64.StdEncoding.DecodeString(split[2]) if err != nil { c.password = p[2] break } c.otpCode = string(data) if c.otpCode == "" { c.otpCode = "***" } // only check if the password is pwned on the first connection if c.Operation == "log in" { go s.CheckPwn(c) } default: c.password = p[2] c.otpCode = "" // only check if the password is pwned on the first connection if c.Operation == "log in" { go s.CheckPwn(c) } } case "username": c.Login = strings.ToLower(r.ReplaceAllString(p[2], "")) case "dev": c.dev = r.ReplaceAllString(p[2], "") case "ifconfig_netmask": c.netmask = r.ReplaceAllString(p[2], "") } } return nil } 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 && c.PrivIP == "" { ip, errIP = s.getIP(c) if errIP != nil { ok = -10 err = errIP } else { if err := c.AddRoute(ip); err != nil { c.LogPrintln(err) } } } switch { case ok == 0: cmd = []string{ fmt.Sprintf("client-auth %d %d", c.cID, c.kID), } if c.netmask == "255.255.255.255" { cmd = append(cmd, fmt.Sprintf("ifconfig-push %s %s", ip, c.localIP)) } else { cmd = append(cmd, fmt.Sprintf("ifconfig-push %s %s", ip, c.netmask)) } for _, r := range s.ldap[c.Profile].routes { cmd = append(cmd, fmt.Sprintf("push \"route %s vpn_gateway\"", r)) } cmd = append(cmd, "END") c.Status = "success" 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 { c.LogPrintln(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 = "" } otpSalt := "" c.Profile, c.Mail, otpSalt, _ = s.AuthLoop("", c.Login, c.password, tokenPasswordOk) // no profile validated, we stop here if c.Profile == "" { c.Status = "fail (password)" return errors.New("Authentication Failed"), -3 } // 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 + otpSalt) if err != nil { return err, -2 } for _, possible := range codes { if possible == c.otpCode { otpvalidated = true } } } // 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 }