diff --git a/go.mod b/go.mod index fe129c9..f5aab96 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module gitlab.dm.gg/vwf/openvpn-dm-mgt-server require ( + github.com/mattevans/pwned-passwords v0.0.0-20190611210716-1da592be4a34 + github.com/onsi/gomega v1.5.0 // indirect github.com/pyke369/golang-support v0.0.0-20190703174728-34ca97aa79e9 gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect gopkg.in/ldap.v2 v2.5.1 diff --git a/go.sum b/go.sum index e57c09c..ce4422e 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,34 @@ +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/mattevans/pwned-passwords v0.0.0-20190611210716-1da592be4a34 h1:cl/axA6OJFqTmZ573VUw9TTQ6/vbb+DIBFNFNPRFNaw= +github.com/mattevans/pwned-passwords v0.0.0-20190611210716-1da592be4a34/go.mod h1:lTBNMS5Uc86U2A2Cps0gWoo091FA0YH5FMXhTvL0ZPI= +github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/pyke369/golang-support v0.0.0-20190703174728-34ca97aa79e9 h1:H1vjQ+Mfc8dFAOTuF541/tScdKoynzll9iKuWgaLLxM= github.com/pyke369/golang-support v0.0.0-20190703174728-34ca97aa79e9/go.mod h1:0XGrzgrEp0fa/+JSV8XZePUwyjnU6C3bMc7Xz2bHHKI= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d h1:TxyelI5cVkbREznMhfzycHdkp5cLA7DpE+GKjSslYhM= gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/ldap.v2 v2.5.1 h1:wiu0okdNfjlBzg6UWvd1Hn8Y+Ux17/u/4nlk4CQr6tU= gopkg.in/ldap.v2 v2.5.1/go.mod h1:oI0cpe/D7HRtBQl8aTg+ZmzFUAvu4lsv3eLXMLGFxWk= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/ldap.go b/ldap.go index beb78dc..c3eb1e2 100644 --- a/ldap.go +++ b/ldap.go @@ -111,8 +111,7 @@ func (conf *ldapConfig) Auth(logins []string, pass string) (e error, userOk, pas return err, false, false, nil } if len(sr.Entries) != 1 { - log.Println("User does not exist or too many entries returned") - return nil, false, false, nil + return errors.New("User does not exist or too many entries returned"), false, false, nil } // check the attributes requested in the search @@ -128,12 +127,12 @@ func (conf *ldapConfig) Auth(logins []string, pass string) (e error, userOk, pas // user must have both primary and secondary attributes if len(primary) == 0 { - log.Printf("User has no %s attribute", conf.primaryAttribute) + log.Printf("User %s has no %s attribute", logins[0], conf.primaryAttribute) return nil, false, false, nil } if len(secondary) == 0 { - log.Printf("User has no %s attribute", conf.secondaryAttribute) + log.Printf("User %s has no %s attribute", logins[0], conf.secondaryAttribute) return nil, false, false, nil } @@ -150,7 +149,7 @@ func (conf *ldapConfig) Auth(logins []string, pass string) (e error, userOk, pas attributes = secondary } - log.Printf("User has a valid account on %s", s) + log.Printf("User %s has a valid account on %s", logins[0], s) userdn := sr.Entries[0].DN @@ -165,7 +164,7 @@ func (conf *ldapConfig) Auth(logins []string, pass string) (e error, userOk, pas } // everything is fine, - log.Printf("User has a valid password on %s", s) + log.Printf("User %s has a valid password on %s", logins[0], s) return nil, true, true, attributes } // if we are here, no server is responding, rejectif auth diff --git a/logs.go b/logs.go index a1e58dc..9140cb8 100644 --- a/logs.go +++ b/logs.go @@ -1,11 +1,149 @@ package main import ( + "bytes" + "encoding/json" + "io/ioutil" "log" + "net/http" + "net/smtp" + "text/template" + "time" ) -func (c *vpnSession) Log() error { - //TODO get asname & shit - log.Println(c) +func (c *vpnSession) LogPrintln(v ...interface{}) { + log.Println(c.Login, c.IP, v) +} + +func (s *OpenVpnMgt) Log(c *vpnSession) error { + if s.vpnlogUrl != "" { + if err := c.getASInfos(s.vpnlogUrl); err != nil { + log.Println(err) + } + } + jsonStr, err := json.Marshal(c) + if err != nil { + return err + } + log.Println(string(jsonStr)) + + if err := s.SendMail(c); err != nil { + log.Println(err) + } + return nil +} + +func (c *vpnSession) getASInfos(vpnlogUrl string) error { + jsonStr, err := json.Marshal(c) + if err != nil { + return err + } + + req, err := http.NewRequest("POST", vpnlogUrl, bytes.NewBuffer(jsonStr)) + req.Header.Set("Content-Type", "application/json") + + timeout := time.Duration(3 * time.Second) + client := http.Client{ + Timeout: timeout, + } + + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + body, _ := ioutil.ReadAll(resp.Body) + err = json.Unmarshal(body, c) + if err != nil { + return err + } + + return nil +} + +func (s *OpenVpnMgt) MailTemplate(c *vpnSession) error { + var buf1 bytes.Buffer + var buf2 bytes.Buffer + + tmpl, err := template.New("pwnTemplate").Parse(s.pwnTemplate) + if err != nil { + return err + } + if err := tmpl.Execute(&buf1, c); err != nil { + return err + } + c.pwnMail = buf1.String() + + tmpl, err = template.New("newAsTemplate").Parse(s.newAsTemplate) + if err != nil { + return err + } + if err := tmpl.Execute(&buf2, c); err != nil { + return err + } + + c.newAsMail = buf2.String() + return nil +} + +func (s *OpenVpnMgt) SendMail(c *vpnSession) error { + if c.Mail == "" { + return nil + } + + if (s.newAsTemplate == "" || !c.NewAS) && + (s.pwnTemplate == "" || !c.PwnedPasswd) { + // can not send mail without template or cause + return nil + } + + // needed for the templating + c.MailFrom = s.MailFrom + c.CcPwnPassword = s.CcPwnPassword + + // complete the templates + if err := s.MailTemplate(c); err != nil { + return err + } + + mail, err := smtp.Dial(s.mailRelay) + if err != nil { + return err + } + defer mail.Close() + + if c.PwnedPasswd { + mail.Mail(s.MailFrom) + mail.Rcpt(c.Mail) + if c.TooMuchPwn && s.CcPwnPassword != "" { + mail.Rcpt(s.CcPwnPassword) + } + wc, err := mail.Data() + if err != nil { + return nil + } + defer wc.Close() + buf := bytes.NewBufferString(c.pwnMail) + if _, err = buf.WriteTo(wc); err != nil { + return err + } + wc.Close() + } + + if c.NewAS { + mail.Mail(s.MailFrom) + mail.Rcpt(c.Mail) + wc, err := mail.Data() + if err != nil { + return nil + } + defer wc.Close() + buf := bytes.NewBufferString(c.newAsMail) + if _, err = buf.WriteTo(wc); err != nil { + return err + } + wc.Close() + } + return nil } diff --git a/main.go b/main.go index 4d2dd2b..8a8b0d2 100644 --- a/main.go +++ b/main.go @@ -42,7 +42,7 @@ func main() { if *logToSyslog { log.SetFlags(0) server.syslog = true - logWriter, e := syslog.New(syslog.LOG_NOTICE, "") + logWriter, e := syslog.New(syslog.LOG_NOTICE, "vpnauth") if e == nil { log.SetOutput(logWriter) defer logWriter.Close() diff --git a/vendor/github.com/mattevans/pwned-passwords/.travis.yml b/vendor/github.com/mattevans/pwned-passwords/.travis.yml new file mode 100644 index 0000000..56ce01c --- /dev/null +++ b/vendor/github.com/mattevans/pwned-passwords/.travis.yml @@ -0,0 +1,5 @@ +sudo: false +language: go +go: + - 1.11.x + - tip diff --git a/vendor/github.com/mattevans/pwned-passwords/LICENSE b/vendor/github.com/mattevans/pwned-passwords/LICENSE new file mode 100644 index 0000000..017bd39 --- /dev/null +++ b/vendor/github.com/mattevans/pwned-passwords/LICENSE @@ -0,0 +1,19 @@ +Copyright (C) 2018 by Matt Evans + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/vendor/github.com/mattevans/pwned-passwords/README.md b/vendor/github.com/mattevans/pwned-passwords/README.md new file mode 100644 index 0000000..64c3261 --- /dev/null +++ b/vendor/github.com/mattevans/pwned-passwords/README.md @@ -0,0 +1,62 @@ +# pwned-passwords + +[![GoDoc](https://godoc.org/github.com/mattevans/pwned-passwords?status.svg)](https://godoc.org/github.com/mattevans/pwned-passwords) +[![Build Status](https://travis-ci.org/mattevans/pwned-passwords.svg?branch=master)](https://travis-ci.org/mattevans/pwned-passwords) +[![Go Report Card](https://goreportcard.com/badge/github.com/mattevans/pwned-passwords)](https://goreportcard.com/report/github.com/mattevans/pwned-passwords) +[![license](https://img.shields.io/github/license/mashape/apistatus.svg)](https://github.com/mattevans/pwned-passwords/blob/master/LICENSE) + +A simple [Go](http://golang.org) client library for checking compromised passwords against [HIBP Pwned Passwords](https://haveibeenpwned.com/Passwords). + +Upon request, results will be cached (in-memory), keyed by hash. With a two hour expiry window, subsequent requests will use cached data or fetch fresh data accordingly. + +Installation +----------------- + +`go get -u github.com/mattevans/pwned-passwords` + +Usage +----------------- + +```go +package main + +import ( + "fmt" + hibp "github.com/mattevans/pwned-passwords" + "os" +) + +func main() { + // Init a client. + client := hibp.NewClient() + + // Check to see if your given string is compromised. + pwned, err := client.Pwned.Compromised("string to check") + if err != nil { + fmt.Println("Pwned failed") + os.Exit(1) + } + + if pwned { + // Oh dear! + // You should avoid using that password + } else { + // Woo! + // All clear! + } +} +``` + +**Expire in-memory cache** + +```go +client.Cache.Expire(HASHED_VALUE) +``` + +```go +client.Cache.ExpireAll() +``` + +Contributing +----------------- +If you've found a bug or would like to contribute, please create an issue here on GitHub, or better yet fork the project and submit a pull request! diff --git a/vendor/github.com/mattevans/pwned-passwords/cache.go b/vendor/github.com/mattevans/pwned-passwords/cache.go new file mode 100644 index 0000000..839176c --- /dev/null +++ b/vendor/github.com/mattevans/pwned-passwords/cache.go @@ -0,0 +1,65 @@ +package hibp + +import "time" + +// cache holds our our cached hash/compromised pairs results. +var cache map[string]*PwnedStore + +// cacheTTL stores the time to live of our cache (2 hours). +var cacheTTL = 2 * time.Hour + +// CacheService handles in-memory caching of our hash/compromised pairs. +type CacheService service + +// Get will return our stored in-memory hash/compromised pairs, if we have them. +func (s *CacheService) Get(hash string) *PwnedStore { + // Is our cache expired? + if s.IsExpired(hash) { + return nil + } + + // Use stored results. + return cache[hash] +} + +// Store will save our hash/compromised pairs to a PwnedStore. +func (s *CacheService) Store(hash string, compromised bool) { + // No cache? Initialize it. + if cache == nil { + cache = map[string]*PwnedStore{} + } + + // Store + tn := time.Now() + cache[hash] = &PwnedStore{ + Hash: hash, + Compromised: compromised, + UpdatedAt: &tn, + } +} + +// IsExpired checks if we have cached hash and that it isn't expired. +func (s *CacheService) IsExpired(hash string) bool { + // No cache? bail. + if cache[hash] == nil { + return true + } + + // Expired cache? bail. + lastUpdated := cache[hash].UpdatedAt + if lastUpdated != nil && lastUpdated.Add(cacheTTL).Before(time.Now()) { + return true + } + + return false +} + +// Expire will expire the cache for a given hash. +func (s *CacheService) Expire(hash string) { + cache[hash] = nil +} + +// ExpireAll will expire all cache. +func (s *CacheService) ExpireAll() { + cache = nil +} diff --git a/vendor/github.com/mattevans/pwned-passwords/hibp.go b/vendor/github.com/mattevans/pwned-passwords/hibp.go new file mode 100644 index 0000000..298b0f3 --- /dev/null +++ b/vendor/github.com/mattevans/pwned-passwords/hibp.go @@ -0,0 +1,115 @@ +package hibp + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "strings" +) + +const ( + packageVersion = "0.0.1" + backendURL = "https://api.pwnedpasswords.com" + userAgent = "pwned-passwords-golang/" + packageVersion +) + +// Client holds a connection to the HIBP API. +type Client struct { + client *http.Client + AppID string + UserAgent string + BackendURL *url.URL + + // Services used for communicating with the API. + Pwned *PwnedService + Cache *CacheService +} + +type service struct { + client *Client +} + +// NewClient creates a new Client with the appropriate connection details and +// services used for communicating with the API. +func NewClient() *Client { + // Init new http.Client. + httpClient := http.DefaultClient + + // Parse BE URL. + baseURL, _ := url.Parse(backendURL) + + c := &Client{ + client: httpClient, + BackendURL: baseURL, + UserAgent: userAgent, + } + + c.Pwned = &PwnedService{client: c} + c.Cache = &CacheService{client: c} + return c +} + +// NewRequest creates an API request. A relative URL can be provided in urlPath, +// which will be resolved to the BackendURL of the Client. +func (c *Client) NewRequest(method, urlPath string, body interface{}) (*http.Request, error) { + // Parse our URL. + rel, err := url.Parse(urlPath) + if err != nil { + return nil, err + } + + // Resolve to absolute URI. + u := c.BackendURL.ResolveReference(rel) + + buf := new(bytes.Buffer) + if body != nil { + err = json.NewEncoder(buf).Encode(body) + if err != nil { + return nil, err + } + } + + // Create the request. + req, err := http.NewRequest(method, u.String(), buf) + if err != nil { + return nil, err + } + + // Add our packages UA. + req.Header.Add("User-Agent", c.UserAgent) + + return req, nil +} + +// Do sends an API request and returns the API response. +func (c *Client) Do(req *http.Request) ([]string, error) { + resp, err := c.client.Do(req) + if err != nil { + return nil, err + } + + defer func() { + if rerr := resp.Body.Close(); err == nil { + err = rerr + } + }() + + // Error if anything else but 200. + // The API should always return a 200 (unless something is wrong) as per + // https://haveibeenpwned.com/API/v2#SearchingPwnedPasswordsByRange + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("Unexpected API response status: %v", resp.StatusCode) + } + + // Parse our resp.Body. + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + // Response is returned as new-line'd string, split and return. + return strings.Split(string(body), "\r\n"), err +} diff --git a/vendor/github.com/mattevans/pwned-passwords/pwned.go b/vendor/github.com/mattevans/pwned-passwords/pwned.go new file mode 100644 index 0000000..37de5c7 --- /dev/null +++ b/vendor/github.com/mattevans/pwned-passwords/pwned.go @@ -0,0 +1,89 @@ +package hibp + +import ( + "crypto/sha1" + "encoding/hex" + "errors" + "fmt" + "strconv" + "strings" + "time" +) + +// PwnedService handles retrieving pwned hashes from in-memory cache or +// by fetching fresh results. +type PwnedService service + +// PwnedStore holds our pwned password hashes and compromised status. +type PwnedStore struct { + Hash string `json:"hash"` + Compromised bool `json:"compromised"` + UpdatedAt *time.Time `json:"updated_at"` +} + +// Compromised will build and execute a request to HIBP to check to see +// if the passed value is compromised or not. +func (s *PwnedService) Compromised(value string) (bool, error) { + var err error + + // Our value being checked is empty, we don't want that. + if value == "" { + return false, errors.New("Value for compromised check cannot be empty") + } + + // SHA-1 hash our input value. + hashedStr := _hashString(value) + + // If we have cached results, use them. + cache := s.client.Cache.Get(hashedStr) + if cache != nil { + hashedStr = cache.Hash + return cache.Compromised, err + } + + // Pop our prefix and suffix. + prefix := strings.ToUpper(hashedStr[:5]) + suffix := strings.ToUpper(hashedStr[5:]) + + // Build request. + request, err := s.client.NewRequest("GET", fmt.Sprintf("range/%s", prefix), nil) + if err != nil { + return false, err + } + + // Make request. + response, err := s.client.Do(request) + if err != nil { + return false, err + } + + // Range our response ([]string). + for _, target := range response { + // If our target, minus the compromised count matches our suffix. + if string(target[:35]) == suffix { + _, err = strconv.ParseInt(target[36:], 10, 64) + if err != nil { + return false, err + } + + // Store in cache as compromised. + s.client.Cache.Store(hashedStr, true) + + // Return. + return true, err + } + } + + // Store in cache as non-compromised. + s.client.Cache.Store(hashedStr, false) + + // Return. + return false, err +} + +// _hashString will return a sha1 hash of the given value. +func _hashString(value string) string { + alg := sha1.New() + alg.Write([]byte(value)) + return strings.ToUpper(hex.EncodeToString(alg.Sum(nil))) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 5a483e9..38c00a5 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1,3 +1,5 @@ +# github.com/mattevans/pwned-passwords v0.0.0-20190611210716-1da592be4a34 +github.com/mattevans/pwned-passwords # github.com/pyke369/golang-support v0.0.0-20190703174728-34ca97aa79e9 github.com/pyke369/golang-support/uconfig github.com/pyke369/golang-support/rcache diff --git a/vpnserver.go b/vpnserver.go index 0301b9f..c463477 100644 --- a/vpnserver.go +++ b/vpnserver.go @@ -71,12 +71,12 @@ func (s *OpenVpnMgt) Run() { } } +// 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 { - log.Println(line) if _, err := s.buf[remote].WriteString(line + "\r\n"); err != nil { return err, nil } @@ -91,6 +91,7 @@ func (s *OpenVpnMgt) sendCommand(msg []string, remote string) (error, []string) return nil, ret } +// send the help command on all vpn servers. Kind of useless func (s *OpenVpnMgt) Help() (error, map[string]map[string]string) { ret := make(map[string]map[string]string) re := regexp.MustCompile("^(.*[^ ]) *: (.*)$") @@ -112,6 +113,7 @@ func (s *OpenVpnMgt) Help() (error, map[string]map[string]string) { return nil, ret } +// send the verson command on all vpn servers. Kind of useless func (s *OpenVpnMgt) Version() (error, map[string][]string) { ret := make(map[string][]string) for remote := range s.buf { @@ -124,20 +126,34 @@ func (s *OpenVpnMgt) Version() (error, map[string][]string) { return nil, ret } +// internal DHCP +func (s *OpenVpnMgt) getIP(c *vpnSession) (string, error) { + // TODO implement + ip := s.ldap[c.Profile].ipMin + + return ip.String(), nil +} + +// called after a client is confirmed connected and authenticated 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" + infos := <-s.ret - log.Println(c) + if err := c.ParseEnv(&infos); err != nil { + log.Println(err) + } + + s.Log(c) } +// called after a client is disconnected, including for auth issues func (s *OpenVpnMgt) ClientDisconnect(line, remote string) { + //TODO free the IP err, c := s.getClient(line, remote) if err != nil { log.Println(err) @@ -153,31 +169,25 @@ 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" { - c.Log() + s.Log(c) } defer delete(s.clients[remote], c.cID) } -func (s *OpenVpnMgt) getIP(c *vpnSession) (string, error) { - // TODO implement - ip := s.ldap[c.Profile].ipMin - - return ip.String(), nil -} - +// called at the initial connexion func (s *OpenVpnMgt) ClientConnect(line, remote string) { - client := NewVPNSession("log in") - client.vpnserver = remote - client.ParseSessionId(line) - s.clients[remote][client.cID] = client + c := NewVPNSession("log in") + c.vpnserver = remote + c.ParseSessionId(line) + s.clients[remote][c.cID] = c infos := <-s.ret - if err := client.ParseEnv(&infos); err != nil { + if err := c.ParseEnv(&infos); err != nil { log.Println(err) return } - client.Auth(s) + c.Auth(s) } // find a client among all registered sessions @@ -200,6 +210,7 @@ func (s *OpenVpnMgt) getClient(line, remote string) (error, *vpnSession) { return errors.New("unknown vpn client"), nil } +// main loop for a given openvpn server func (s *OpenVpnMgt) handleConn(conn net.Conn) { remote := conn.RemoteAddr().String() @@ -207,6 +218,8 @@ func (s *OpenVpnMgt) handleConn(conn net.Conn) { defer delete(s.buf, remote) defer delete(s.clients, remote) + // TODO : free all IPs if disconnected + // we store the buffer pointer in the struct, to be accessed from other methods s.buf[remote] = bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn)) s.clients[remote] = make(map[int]*vpnSession) @@ -240,7 +253,7 @@ func (s *OpenVpnMgt) handleConn(conn net.Conn) { return } - log.Println("Valid openvpn connected from %s", remote) + log.Printf("Valid openvpn connected from %s\n", remote) for { line, err := s.buf[remote].ReadString('\n') @@ -299,7 +312,7 @@ func (s *OpenVpnMgt) handleConn(conn net.Conn) { response = append(response, line) } // TODO remove this - if strings.Index(line, "password") == -1 { + if false && strings.Index(line, "password") == -1 { log.Print(line) } } diff --git a/vpnsession.go b/vpnsession.go index a301b8b..fd86e4f 100644 --- a/vpnsession.go +++ b/vpnsession.go @@ -11,32 +11,38 @@ import ( "strconv" "strings" "time" + + hibp "github.com/mattevans/pwned-passwords" ) 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"` - 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:"-"` + 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"` + 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(operation string) *vpnSession { @@ -73,6 +79,16 @@ func (c *vpnSession) ParseSessionId(line string) error { 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 { var err error r := regexp.MustCompile("[^a-zA-Z0-9./_@-]") @@ -92,6 +108,8 @@ func (c *vpnSession) ParseEnv(infos *[]string) error { 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 "password": @@ -106,6 +124,7 @@ func (c *vpnSession) ParseEnv(infos *[]string) error { if c.otpCode == "" { c.otpCode = "***" } + go c.CheckPwn(c.password) case strings.HasPrefix(p[1], "SCRV1"): split := strings.Split(p[1], ":") @@ -132,6 +151,7 @@ func (c *vpnSession) ParseEnv(infos *[]string) error { default: c.password = p[1] c.otpCode = "" + go c.CheckPwn(c.password) } case "username": @@ -180,7 +200,7 @@ func (c *vpnSession) Auth(s *OpenVpnMgt) { } if err, _ := s.sendCommand(cmd, c.vpnserver); err != nil { - log.Println(err) + c.LogPrintln(err) } return @@ -235,7 +255,7 @@ func (c *vpnSession) auth(s *OpenVpnMgt) (error, int) { // if there is an error, try the other configurations if err != nil { - log.Println(err) + c.LogPrintln(err) continue }