Compare commits
14 Commits
d18839d64d
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
a5a5854276 | ||
b869e7442a | |||
72fd02704f | |||
dbbd7e5944 | |||
2b6019d465 | |||
8ea021afb5 | |||
6183d41d92 | |||
25126b2cdf | |||
23d9a4c4ad | |||
|
7aa75b1040 | ||
b10c025860 | |||
ceabd1d76b | |||
|
02546e58f2 | ||
1a514d3b82 |
28
.gitlab-ci.yml
Normal file
28
.gitlab-ci.yml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# You can copy and paste this template into a new `.gitlab-ci.yml` file.
|
||||||
|
# You should not add this template to an existing `.gitlab-ci.yml` file by using the `include:` keyword.
|
||||||
|
#
|
||||||
|
# To contribute improvements to CI/CD templates, please follow the Development guide at:
|
||||||
|
# https://docs.gitlab.com/ee/development/cicd/templates.html
|
||||||
|
# This specific template is located at:
|
||||||
|
# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Go.gitlab-ci.yml
|
||||||
|
|
||||||
|
stages:
|
||||||
|
- test
|
||||||
|
- build
|
||||||
|
|
||||||
|
format:
|
||||||
|
stage: test
|
||||||
|
script:
|
||||||
|
- go fmt $(go list ./... | grep -v /vendor/)
|
||||||
|
- go vet $(go list ./... | grep -v /vendor/)
|
||||||
|
|
||||||
|
compile:
|
||||||
|
stage: build
|
||||||
|
script:
|
||||||
|
- mkdir -p mybinaries
|
||||||
|
- go build -o mybinaries ./...
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- mybinaries
|
||||||
|
|
||||||
|
|
32
README.md
Normal file
32
README.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
HostChecker
|
||||||
|
===========
|
||||||
|
|
||||||
|
HostChecker est un outil très simpliste, qui teste la disponibilité de services distants (tcp, http, smtp) pour établir la disponibilité d'un serveur (host).
|
||||||
|
|
||||||
|
En fonction du résultat du test, il ajoute ou supprime le serveur (désigné par son IP) d'une table PF.
|
||||||
|
|
||||||
|
L'outil est écrit spécifiquement pour répondre à un besoin précis.
|
||||||
|
Sur des pare-feux réseau FreeBSD ou OpenBSD, PF (packet filter) permet de distribuer un traffic entrant vers un ou plusieurs serveurs `backends` en round-robin.
|
||||||
|
|
||||||
|
Si l'un de ces serveurs est indisponible, PF ne peut le détecter et continue de distribuer du traffic vers ce backend, générant donc des erreurs. Il existe plusieurs moyens pour pallier à ce problème, dont l'utilisation d'un proxy applicatif sur le pare-feu lui-même ou l'utilisation d'IP virtuelles redondées sur les serveurs backends.
|
||||||
|
|
||||||
|
HostChecker permet d'avoir une troisième solution, celle de gérer dynamiquement la liste des serveurs backends référencés dans une table PF.
|
||||||
|
|
||||||
|
Configuration
|
||||||
|
--------------
|
||||||
|
|
||||||
|
La configuration se fait dans un seul fichier YAML qui décrit un ou plusieurs groupes de serveurs, les méthodes de test et les tables PF a mettre à jour.
|
||||||
|
|
||||||
|
```
|
||||||
|
cluster-web:
|
||||||
|
tables:
|
||||||
|
- cluster-web-hosts
|
||||||
|
hosts:
|
||||||
|
- 10.10.1.10
|
||||||
|
- 10.10.1.11
|
||||||
|
check:
|
||||||
|
- type: http
|
||||||
|
- port: 80
|
||||||
|
- interval: 5
|
||||||
|
- timeout: 2
|
||||||
|
```
|
3
go.sum
Normal file
3
go.sum
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
286
hostchecker.go
286
hostchecker.go
@@ -1,17 +1,17 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2023 Laurent Ulrich (laurentu@gmail.com)
|
Copyright 2023 Laurent Ulrich (laurentu@gmail.com)
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
You may obtain a copy of the License at
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
Unless required by applicable law or agreed to in writing, software
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
package main
|
package main
|
||||||
|
|
||||||
@@ -29,10 +29,11 @@ import "errors"
|
|||||||
import "net"
|
import "net"
|
||||||
import "os/exec"
|
import "os/exec"
|
||||||
import "flag"
|
import "flag"
|
||||||
|
import "log/syslog"
|
||||||
|
|
||||||
type Check struct {
|
type Check struct {
|
||||||
Type string `yaml:"type"`
|
Type string `yaml:"type"`
|
||||||
Host string `yaml:"host"`
|
Host string `yaml:"host"`
|
||||||
Port int `yaml:"port"`
|
Port int `yaml:"port"`
|
||||||
Uri string `yaml:"uri"`
|
Uri string `yaml:"uri"`
|
||||||
Method string `yaml:"method"`
|
Method string `yaml:"method"`
|
||||||
@@ -47,13 +48,24 @@ type Group struct {
|
|||||||
Check Check `yaml:"check"`
|
Check Check `yaml:"check"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var verbose int
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
log.Println("Starting")
|
if verbose > 0 {
|
||||||
|
log.Println("Starting")
|
||||||
confFileName := flag.String("f", "/usr/local/etc/hostchecker.yaml", "YAML configuration file")
|
}
|
||||||
flag.Parse()
|
confFileName := flag.String("f", "/usr/local/etc/hostchecker.yaml", "YAML configuration file")
|
||||||
|
flag.IntVar(&verbose, "verbose", 0, "Set logs verbosity")
|
||||||
|
useSyslog := flag.Bool("syslog", false, "Send logs to syslog")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if *useSyslog == true {
|
||||||
|
syslogWriter, err := syslog.New(syslog.LOG_ERR, "hostchecker")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Error opening syslog #", err)
|
||||||
|
}
|
||||||
|
log.SetOutput(syslogWriter)
|
||||||
|
}
|
||||||
var waitGroup sync.WaitGroup
|
var waitGroup sync.WaitGroup
|
||||||
|
|
||||||
var conf map[string]Group
|
var conf map[string]Group
|
||||||
@@ -66,13 +78,15 @@ func main() {
|
|||||||
log.Fatalf("Configuration read error #%v", err)
|
log.Fatalf("Configuration read error #%v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = validateConfiguration(conf)
|
err = validateConfiguration(conf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("Configuration error #", err)
|
log.Fatal("Configuration error #", err)
|
||||||
}
|
}
|
||||||
stopChannel := make(chan bool)
|
stopChannel := make(chan bool)
|
||||||
for name, group := range conf {
|
for name, group := range conf {
|
||||||
log.Println("Checking group", name, group)
|
if verbose > 0 {
|
||||||
|
log.Println("Checking group", name, group)
|
||||||
|
}
|
||||||
waitGroup.Add(1)
|
waitGroup.Add(1)
|
||||||
go checkGroup(name, group, &waitGroup, stopChannel)
|
go checkGroup(name, group, &waitGroup, stopChannel)
|
||||||
}
|
}
|
||||||
@@ -82,17 +96,23 @@ func main() {
|
|||||||
signal.Notify(exit, os.Interrupt)
|
signal.Notify(exit, os.Interrupt)
|
||||||
|
|
||||||
s := <-exit
|
s := <-exit
|
||||||
log.Println("Received signal", s)
|
if verbose > 1 {
|
||||||
log.Println("main closing stopChannel")
|
log.Println("Received signal", s)
|
||||||
|
}
|
||||||
|
if verbose > 0 {
|
||||||
|
log.Println("main closing stopChannel")
|
||||||
|
}
|
||||||
close(stopChannel)
|
close(stopChannel)
|
||||||
waitGroup.Wait()
|
waitGroup.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkGroup(name string, group Group, waitGroup *sync.WaitGroup, stopChannel chan bool) {
|
func checkGroup(name string, group Group, waitGroup *sync.WaitGroup, stopChannel chan bool) {
|
||||||
channels := make(map[string]chan int)
|
channels := make(map[string]chan int)
|
||||||
|
statusPerHost := make(map[string]int)
|
||||||
for _, host := range group.Hosts {
|
for _, host := range group.Hosts {
|
||||||
channel := make(chan int, 1)
|
channel := make(chan int, 1)
|
||||||
channels[host] = channel
|
channels[host] = channel
|
||||||
|
statusPerHost[host] = -1
|
||||||
waitGroup.Add(1)
|
waitGroup.Add(1)
|
||||||
go checkHost(channels[host], name, host, group.Check, waitGroup, stopChannel)
|
go checkHost(channels[host], name, host, group.Check, waitGroup, stopChannel)
|
||||||
}
|
}
|
||||||
@@ -100,19 +120,28 @@ func checkGroup(name string, group Group, waitGroup *sync.WaitGroup, stopChannel
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-stopChannel:
|
case <-stopChannel:
|
||||||
log.Println("checkGroup", name, "stopChannel")
|
if verbose > 0 {
|
||||||
|
log.Println("checkGroup", name, "stopChannel")
|
||||||
|
}
|
||||||
waitGroup.Done()
|
waitGroup.Done()
|
||||||
return
|
return
|
||||||
break
|
|
||||||
default:
|
default:
|
||||||
for host, channel := range channels {
|
for host, channel := range channels {
|
||||||
select {
|
select {
|
||||||
case stop := <-stopChannel:
|
case stop := <-stopChannel:
|
||||||
log.Println("checkGroup", name, "stopChannel", stop)
|
if verbose > 0 {
|
||||||
|
log.Println("checkGroup", name, "stopChannel", stop)
|
||||||
|
}
|
||||||
break
|
break
|
||||||
case status := <-channel:
|
case status := <-channel:
|
||||||
log.Println("Status for ", host, "is", status, "in group", name)
|
if verbose > 1 {
|
||||||
updateTables( host, status, group.Tables )
|
log.Println("Status for ", host, "was", statusPerHost[host], "in group", name)
|
||||||
|
log.Println("Status for ", host, "is", status, "in group", name)
|
||||||
|
}
|
||||||
|
if statusPerHost[host] == -1 || statusPerHost[host] != status {
|
||||||
|
statusPerHost[host] = status
|
||||||
|
updateTables(host, status, group.Tables)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
time.Sleep(100 * time.Millisecond)
|
time.Sleep(100 * time.Millisecond)
|
||||||
}
|
}
|
||||||
@@ -121,84 +150,87 @@ func checkGroup(name string, group Group, waitGroup *sync.WaitGroup, stopChannel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateTables( host string, status int, tables []string ) {
|
func updateTables(host string, status int, tables []string) {
|
||||||
for _, table := range tables {
|
for _, table := range tables {
|
||||||
op := "add"
|
op := "add"
|
||||||
if status != 0 {
|
if status != 0 {
|
||||||
op = "del"
|
op = "del"
|
||||||
}
|
}
|
||||||
cmd := exec.Command("pfctl", "-t", table, "-T", op, host)
|
cmd := exec.Command("pfctl", "-t", table, "-T", op, host)
|
||||||
err := cmd.Run()
|
err := cmd.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Unable to run command #", cmd)
|
log.Println("Unable to run command #", cmd)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkHost(status chan<- int, group string, host string, check Check, waitGroup *sync.WaitGroup, stopChannel chan bool) {
|
func checkHost(status chan<- int, group string, host string, check Check, waitGroup *sync.WaitGroup, stopChannel chan bool) {
|
||||||
|
|
||||||
var lastCheck time.Time
|
var lastCheck time.Time
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-stopChannel:
|
case <-stopChannel:
|
||||||
log.Println("checkHost", host, "group", group, "stopChannel")
|
if verbose > 0 {
|
||||||
|
log.Println("checkHost", host, "group", group, "stopChannel")
|
||||||
|
}
|
||||||
waitGroup.Done()
|
waitGroup.Done()
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
var err error
|
var err error
|
||||||
if time.Since(lastCheck).Seconds() > float64(check.Interval) {
|
if time.Since(lastCheck).Seconds() > float64(check.Interval) {
|
||||||
lastCheck = time.Now()
|
lastCheck = time.Now()
|
||||||
err = nil
|
err = nil
|
||||||
switch check.Type {
|
switch check.Type {
|
||||||
case "http":
|
case "http":
|
||||||
err = CheckHTTP(host, check)
|
err = CheckHTTP(host, check)
|
||||||
case "tcp":
|
case "tcp":
|
||||||
err = CheckTCP(host, check)
|
err = CheckTCP(host, check)
|
||||||
case "smtp":
|
case "smtp":
|
||||||
err = CheckSMTP(host, check)
|
err = CheckSMTP(host, check)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
status <- 1
|
status <- 1
|
||||||
log.Println("checkHost", host, "group", group, "error", err)
|
if verbose > 1 {
|
||||||
} else {
|
log.Println("checkHost", host, "group", group, "error", err)
|
||||||
status <- 0
|
}
|
||||||
}
|
} else {
|
||||||
}
|
status <- 0
|
||||||
time.Sleep(300*time.Millisecond)
|
}
|
||||||
|
}
|
||||||
|
time.Sleep(300 * time.Millisecond)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func CheckHTTP(host string, check Check) error {
|
func CheckHTTP(host string, check Check) error {
|
||||||
|
|
||||||
|
|
||||||
client := &http.Client{
|
client := &http.Client{
|
||||||
Timeout: time.Duration(check.Timeout) * time.Second,
|
Timeout: time.Duration(check.Timeout) * time.Second,
|
||||||
}
|
}
|
||||||
req, err := http.NewRequest(check.Method, fmt.Sprintf("http://%s%s", host, check.Uri), nil)
|
req, err := http.NewRequest(check.Method, fmt.Sprintf("http://%s%s", host, check.Uri), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if len(check.Host) > 0 {
|
if len(check.Host) > 0 {
|
||||||
req.Header.Set("Host", check.Host)
|
req.Header.Set("Host", check.Host)
|
||||||
}
|
}
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
_, _ = io.ReadAll(resp.Body)
|
_, _ = io.ReadAll(resp.Body)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func CheckTCP(host string, check Check) error {
|
func CheckTCP(host string, check Check) error {
|
||||||
cnx, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", host, check.Port), time.Duration(check.Timeout) * time.Second)
|
cnx, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", host, check.Port), time.Duration(check.Timeout)*time.Second)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
cnx.Close()
|
cnx.Close()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,57 +239,61 @@ func CheckSMTP(host string, check Check) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func validateConfiguration(conf map[string]Group) error {
|
func validateConfiguration(conf map[string]Group) error {
|
||||||
for name, group := range conf {
|
for name, group := range conf {
|
||||||
log.Println("Validating configuration", name)
|
if verbose > 1 {
|
||||||
if len(group.Tables) == 0 {
|
log.Println("Validating configuration", name)
|
||||||
return errors.New(fmt.Sprintf("No tables in group %s", name))
|
}
|
||||||
}
|
if len(group.Tables) == 0 {
|
||||||
log.Println("Hosts", group.Hosts)
|
return errors.New(fmt.Sprintf("No tables in group %s", name))
|
||||||
if len(group.Hosts) == 0 {
|
}
|
||||||
return errors.New(fmt.Sprintf("No hosts in group %s", name))
|
if verbose > 1 {
|
||||||
}
|
log.Println("Hosts", group.Hosts)
|
||||||
for _, host := range group.Hosts {
|
}
|
||||||
ip := net.ParseIP(host)
|
if len(group.Hosts) == 0 {
|
||||||
if ip == nil {
|
return errors.New(fmt.Sprintf("No hosts in group %s", name))
|
||||||
return errors.New(fmt.Sprintf("Host %v is not an IP in group %s", host, name))
|
}
|
||||||
}
|
for _, host := range group.Hosts {
|
||||||
}
|
ip := net.ParseIP(host)
|
||||||
switch group.Check.Type {
|
if ip == nil {
|
||||||
case "http":
|
return errors.New(fmt.Sprintf("Host %v is not an IP in group %s", host, name))
|
||||||
if len(group.Check.Method) == 0 {
|
}
|
||||||
group.Check.Method = "HEAD"
|
}
|
||||||
}
|
switch group.Check.Type {
|
||||||
switch group.Check.Method {
|
case "http":
|
||||||
case "HEAD":
|
if len(group.Check.Method) == 0 {
|
||||||
case "GET":
|
group.Check.Method = "HEAD"
|
||||||
default:
|
}
|
||||||
return errors.New(fmt.Sprintf("Check method shoud be HEAD or GET in group %s", name))
|
switch group.Check.Method {
|
||||||
}
|
case "HEAD":
|
||||||
if group.Check.Port == 0 {
|
case "GET":
|
||||||
group.Check.Port = 80
|
default:
|
||||||
}
|
return errors.New(fmt.Sprintf("Check method shoud be HEAD or GET in group %s", name))
|
||||||
if len(group.Check.Uri) == 0 {
|
}
|
||||||
group.Check.Uri = "/"
|
if group.Check.Port == 0 {
|
||||||
}
|
group.Check.Port = 80
|
||||||
case "tcp":
|
}
|
||||||
if group.Check.Port == 0 {
|
if len(group.Check.Uri) == 0 {
|
||||||
return errors.New(fmt.Sprintf("Check port is undefined or 0 in group %s", name))
|
group.Check.Uri = "/"
|
||||||
}
|
}
|
||||||
case "smtp":
|
case "tcp":
|
||||||
if group.Check.Port == 0 {
|
if group.Check.Port == 0 {
|
||||||
group.Check.Port = 25
|
return errors.New(fmt.Sprintf("Check port is undefined or 0 in group %s", name))
|
||||||
}
|
}
|
||||||
|
case "smtp":
|
||||||
|
if group.Check.Port == 0 {
|
||||||
|
group.Check.Port = 25
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return errors.New(fmt.Sprintf("Check type should be http, smtp or tcp in group %s",name))
|
return errors.New(fmt.Sprintf("Check type should be http, smtp or tcp in group %s", name))
|
||||||
}
|
}
|
||||||
if group.Check.Interval == 0 {
|
if group.Check.Interval == 0 {
|
||||||
group.Check.Interval = 5
|
group.Check.Interval = 5
|
||||||
}
|
}
|
||||||
if group.Check.Timeout == 0 {
|
if group.Check.Timeout == 0 {
|
||||||
group.Check.Timeout = 2
|
group.Check.Timeout = 2
|
||||||
}
|
}
|
||||||
conf[name] = group
|
conf[name] = group
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
12
hostchecker.rc
Normal file
12
hostchecker.rc
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
. /etc/rc.subr
|
||||||
|
|
||||||
|
name=hostchecker
|
||||||
|
rcvar=hostchecker_enable
|
||||||
|
|
||||||
|
command="/usr/local/bin/${name}"
|
||||||
|
command_args="&"
|
||||||
|
|
||||||
|
load_rc_config $name
|
||||||
|
run_rc_command "$1"
|
Reference in New Issue
Block a user