openvpn-mgt/vpnsession.go

318 lines
7.7 KiB
Go

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 = apennd(cmd, fmt.Sprintf("ifconfig-push %s %s", ip, c.localIP))
} else {
cmd = apennd(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
}