diff --git a/main.go b/main.go index 850852e..adc084c 100644 --- a/main.go +++ b/main.go @@ -15,9 +15,10 @@ import ( func main() { var err error var config *uconfig.UConfig - // default configuration file is ./openvpn-dm-mgt-server.conf - configFile := flag.String("config", "openvpn-dm-mgt-server.conf", "configuration file") + // default configuration file is /etc/openvpn/dm-mgt-server.conf + configFile := flag.String("config", "/etc/openvpn/dm-mgt-server.conf", "configuration file") logToSyslog := flag.Bool("syslog", false, "Log to syslog") + debug := flag.Bool("debug", false, "log every message received") flag.Parse() // parseconfig @@ -39,6 +40,8 @@ func main() { server.cacheDir = config.GetString("config.cacheDir", "") server.authCa = config.GetString("config.authCa", "") server.otpMasterSecrets = parseConfigArray(config, "config.masterSecrets") + server.ipRouteScript = config.GetString("config.ipRouteScript", "/bin/ip") + if len(server.otpMasterSecrets) == 0 { server.otpMasterSecrets = append(server.otpMasterSecrets, "*******************") } @@ -54,6 +57,11 @@ func main() { } } + server.debug = false + if *debug { + server.debug = true + } + for _, profile := range config.GetPaths("config.profiles") { profileName := strings.Split(profile, ".")[2] ldapConf := ldapConfig{ diff --git a/openvpn-dm-mgt-server.conf.example b/openvpn-dm-mgt-server.conf.example index 5233a19..4fe1789 100644 --- a/openvpn-dm-mgt-server.conf.example +++ b/openvpn-dm-mgt-server.conf.example @@ -83,6 +83,7 @@ config } openvpnPort: "127.0.0.1:4000" httpPort: ":8443" + ipRouteScript: "./iproute" httpCa: "/usr/local/share/ca-certificates/Dailymotion.crt" httpKey: "/etc/ssl/private/server-key.pem" httpCert: "/etc/ssl/certs/server-bundle.pem" diff --git a/vpnserver.go b/vpnserver.go index 1ae2c2e..6d81d6b 100644 --- a/vpnserver.go +++ b/vpnserver.go @@ -11,11 +11,13 @@ import ( "strconv" "strings" "sync" + + hibp "github.com/mattevans/pwned-passwords" ) // Server represents the server type OpenVpnMgt struct { - Port string + port string buf map[string]*bufio.ReadWriter m sync.RWMutex ret chan []string @@ -30,24 +32,28 @@ type OpenVpnMgt struct { 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), + 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) + addrs, err := net.ResolveTCPAddr("tcp", s.port) if err != nil { log.Println(err) os.Exit(1) @@ -71,12 +77,25 @@ func (s *OpenVpnMgt) Run() { } } +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 } @@ -88,6 +107,13 @@ func (s *OpenVpnMgt) sendCommand(msg []string, remote string) (error, []string) // wait for the response ret := <-s.ret + + if s.debug { + for _, line := range ret { + log.Println(line) + } + } + return nil, ret } @@ -136,7 +162,7 @@ func (s *OpenVpnMgt) ClientValidated(line, remote string) { c.Status = "success" infos := <-s.ret - if err := c.ParseEnv(&infos); err != nil { + if err := c.ParseEnv(s, &infos); err != nil { log.Println(err) } @@ -159,7 +185,8 @@ func (s *OpenVpnMgt) ClientDisconnect(line, remote string) { } // Don't log the initial auth failure due to absence of OTP code - if c.Status != "Need 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) } @@ -168,12 +195,12 @@ func (s *OpenVpnMgt) ClientDisconnect(line, remote string) { // called at the initial connexion func (s *OpenVpnMgt) ClientConnect(line, remote string) { - c := NewVPNSession("log in") + c := NewVPNSession() c.vpnserver = remote c.ParseSessionId(line) s.clients[remote][c.cID] = c infos := <-s.ret - if err := c.ParseEnv(&infos); err != nil { + if err := c.ParseEnv(s, &infos); err != nil { log.Println(err) return } @@ -181,6 +208,27 @@ func (s *OpenVpnMgt) ClientConnect(line, remote string) { 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]*") @@ -201,6 +249,25 @@ func (s *OpenVpnMgt) getClient(line, remote string) (error, *vpnSession) { 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() @@ -282,7 +349,7 @@ func (s *OpenVpnMgt) handleConn(conn net.Conn) { // trafic stats case strings.HasPrefix(line, ">BYTECOUNT_CLI"): - //TODO use that + go s.updateCounters(line, remote) // new bloc for a disconnect event. // We start the receiving handler, which will wait for the Channel message @@ -297,8 +364,14 @@ func (s *OpenVpnMgt) handleConn(conn net.Conn) { 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) + } } } diff --git a/vpnsession.go b/vpnsession.go index 9e1e0f8..6e13af5 100644 --- a/vpnsession.go +++ b/vpnsession.go @@ -7,12 +7,11 @@ import ( "fmt" "log" "os" + "os/exec" "regexp" "strconv" "strings" "time" - - hibp "github.com/mattevans/pwned-passwords" ) type vpnSession struct { @@ -30,6 +29,8 @@ type vpnSession struct { 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:"-"` @@ -45,11 +46,11 @@ type vpnSession struct { CcPwnPassword string `json:"-"` } -func NewVPNSession(operation string) *vpnSession { +func NewVPNSession() *vpnSession { v := vpnSession{ Time: time.Now().Round(time.Second), Status: "system failure", - Operation: operation, + Operation: "log in", } v.Hostname, _ = os.Hostname() @@ -71,29 +72,29 @@ 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 - client_id := strings.Split(strings.Replace(line, ">CLIENT:CONNECT,", "", 1), ",") - if c.cID, err = strconv.Atoi(client_id[0]); err != nil { + 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(client_id[1]); err != nil { + if c.kID, err = strconv.Atoi(match[2]); err != nil { return err } return nil } -func (c *vpnSession) CheckPwn(password string) error { - client := hibp.NewClient() - pwned, err := client.Pwned.Compromised(password) - if err != nil { - return err - } - c.PwnedPasswd = pwned - return nil -} - -func (c *vpnSession) ParseEnv(infos *[]string) error { +func (c *vpnSession) ParseEnv(s *OpenVpnMgt, infos *[]string) error { var err error r := regexp.MustCompile("[^a-zA-Z0-9./_@-]") @@ -128,7 +129,7 @@ func (c *vpnSession) ParseEnv(infos *[]string) error { if c.otpCode == "" { c.otpCode = "***" } - go c.CheckPwn(c.password) + // don't check that password agains the ibp database case strings.HasPrefix(p[1], "SCRV1"): split := strings.Split(p[1], ":") @@ -151,11 +152,17 @@ func (c *vpnSession) ParseEnv(infos *[]string) error { 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 = "" - go c.CheckPwn(c.password) + // only check if the password is pwned on the first connection + if c.Operation == "log in" { + go s.CheckPwn(c) + } } case "username": @@ -174,11 +181,15 @@ func (c *vpnSession) Auth(s *OpenVpnMgt) { err, ok := c.auth(s) // if auth is ok, time to get an IP address - if ok == 0 { + 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) + } } } @@ -192,6 +203,7 @@ func (c *vpnSession) Auth(s *OpenVpnMgt) { 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\"",