300 lines
7.2 KiB
Go
300 lines
7.2 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"regexp"
|
|
"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:"-"`
|
|
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(script, ip string) error {
|
|
cmd := exec.Command(script, "route", "replace", ip, "dev", c.dev)
|
|
return cmd.Run()
|
|
}
|
|
|
|
func (c *vpnSession) ParseSessionId(line string) error {
|
|
var err error
|
|
re := regexp.MustCompile("^>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 := regexp.MustCompile("[^a-zA-Z0-9./_@-]")
|
|
|
|
for _, line := range *infos {
|
|
p := strings.Split(strings.Replace(line, ">CLIENT:ENV,", "", 1), "=")
|
|
switch p[0] {
|
|
case "trusted_port":
|
|
if c.port, err = strconv.Atoi(r.ReplaceAllString(p[1], "")); err != nil {
|
|
return err
|
|
}
|
|
case "untrusted_port":
|
|
if c.port, err = strconv.Atoi(r.ReplaceAllString(p[1], "")); err != nil {
|
|
return err
|
|
}
|
|
case "trusted_ip":
|
|
c.IP = r.ReplaceAllString(p[1], "")
|
|
case "untrusted_ip":
|
|
c.IP = r.ReplaceAllString(p[1], "")
|
|
case "ifconfig_pool_remote_ip":
|
|
c.PrivIP = r.ReplaceAllString(p[1], "")
|
|
case "ifconfig_local":
|
|
c.localIP = r.ReplaceAllString(p[1], "")
|
|
case "bytes_received":
|
|
if c.BwWrite, err = strconv.Atoi(p[1]); err != nil {
|
|
break
|
|
}
|
|
case "bytes_sent":
|
|
if c.BwRead, err = strconv.Atoi(p[1]); err != nil {
|
|
break
|
|
}
|
|
case "password":
|
|
switch {
|
|
case strings.HasPrefix(p[1], "CRV1"):
|
|
split := strings.Split(p[1], ":")
|
|
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[1], "SCRV1"):
|
|
split := strings.Split(p[1], ":")
|
|
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[1]
|
|
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[1]
|
|
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 = r.ReplaceAllString(p[1], "")
|
|
case "dev":
|
|
c.dev = r.ReplaceAllString(p[1], "")
|
|
}
|
|
}
|
|
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(s.ipRouteScript, ip); err != nil {
|
|
c.LogPrintln(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
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")
|
|
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 = ""
|
|
}
|
|
|
|
// 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, c.Mail = 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
|
|
}
|
|
|
|
// 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
|
|
}
|