Add the Blacktip edge
The edge: per-IP TCP/UDP/HTTP/HTTPS/TLS listeners with port ranges and PROXY protocol, WireGuard tunnel endpoint, logins in front of web ports (built-in proxy or Traefik), health checks and failover, and mail egress with DKIM signing and MTA-STS. Split out of the original single repository; depends on Reef for the shared types. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
995ef9a6aa
commit
9448a532e9
17 files changed
+3492
-1
No files matched your search
@@ -0,0 +1,132 @@
|
||||
package blacktip
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.veillax.com/Veillax/reef/model"
|
||||
)
|
||||
|
||||
// healthLoop checks every target of ms until ctx is cancelled. Routing prefers
|
||||
// healthy targets, so a dead backend is skipped without anyone editing config.
|
||||
func (b *Blacktip) healthLoop(ctx context.Context, ms *mapState) {
|
||||
every := time.Duration(ms.m.HealthSeconds) * time.Second
|
||||
if ms.m.HealthSeconds <= 0 {
|
||||
every = 30 * time.Second
|
||||
}
|
||||
run := func() {
|
||||
results := make([]model.TargetHealth, len(ms.m.Targets))
|
||||
var wg sync.WaitGroup
|
||||
for i := range ms.m.Targets {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
results[i] = ms.check(ctx, i)
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
ms.mu.Lock()
|
||||
changed := false
|
||||
for i, r := range results {
|
||||
if ms.health[i].Healthy != r.Healthy {
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
ms.health = results
|
||||
ms.mu.Unlock()
|
||||
if changed {
|
||||
go b.report()
|
||||
}
|
||||
}
|
||||
// healthy targets are re-checked every `every`; unhealthy ones every few
|
||||
// seconds so recovery (or a site that was still connecting) shows up fast
|
||||
run()
|
||||
for {
|
||||
wait := every
|
||||
ms.mu.Lock()
|
||||
for _, h := range ms.health {
|
||||
if !h.Healthy && wait > 5*time.Second {
|
||||
wait = 5 * time.Second
|
||||
}
|
||||
}
|
||||
ms.mu.Unlock()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(wait):
|
||||
run()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ms *mapState) check(ctx context.Context, i int) model.TargetHealth {
|
||||
t := ms.m.Targets[i]
|
||||
if model.Web(ms.m.Proto) && ms.m.HealthPath != "" {
|
||||
return ms.checkHTTP(ctx, t)
|
||||
}
|
||||
c, tunneled, err := ms.b.dialTarget(t, "tcp")
|
||||
if err != nil {
|
||||
return model.TargetHealth{Detail: err.Error()}
|
||||
}
|
||||
defer c.Close()
|
||||
if tunneled {
|
||||
// A tunnel stream opens even when the far side can't connect, but the
|
||||
// agent then closes it straight away. Silence means the target is up.
|
||||
c.SetReadDeadline(time.Now().Add(800 * time.Millisecond))
|
||||
one := make([]byte, 1)
|
||||
if _, err := c.Read(one); err != nil {
|
||||
if ne, ok := err.(net.Error); ok && ne.Timeout() {
|
||||
return model.TargetHealth{Healthy: true}
|
||||
}
|
||||
if err == io.EOF || strings.Contains(err.Error(), "closed") || strings.Contains(err.Error(), "reset") {
|
||||
return model.TargetHealth{Detail: "the site couldn't connect to the target"}
|
||||
}
|
||||
return model.TargetHealth{Detail: err.Error()}
|
||||
}
|
||||
}
|
||||
return model.TargetHealth{Healthy: true}
|
||||
}
|
||||
|
||||
func (ms *mapState) checkHTTP(ctx context.Context, t model.Target) model.TargetHealth {
|
||||
scheme := "http"
|
||||
if ms.m.BackendHTTPS {
|
||||
scheme = "https"
|
||||
}
|
||||
tr := &http.Transport{
|
||||
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||
c, _, err := ms.b.dialTarget(t, "tcp")
|
||||
return c, err
|
||||
},
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
DisableKeepAlives: true,
|
||||
}
|
||||
defer tr.CloseIdleConnections()
|
||||
ctx, cancel := context.WithTimeout(ctx, 6*time.Second)
|
||||
defer cancel()
|
||||
req, _ := http.NewRequestWithContext(ctx, "GET", scheme+"://target"+ms.m.HealthPath, nil)
|
||||
if ms.m.RewriteHost != "" {
|
||||
req.Host = ms.m.RewriteHost
|
||||
} else if h := strings.TrimPrefix(ms.m.Hostname, "*."); h != "" {
|
||||
req.Host = h
|
||||
}
|
||||
req.Header.Set("User-Agent", "blacktip-healthcheck")
|
||||
resp, err := (&http.Client{Transport: tr, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}).Do(req)
|
||||
if err != nil {
|
||||
return model.TargetHealth{Detail: err.Error()}
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode >= 200 && resp.StatusCode < 400 {
|
||||
return model.TargetHealth{Healthy: true}
|
||||
}
|
||||
return model.TargetHealth{Detail: fmt.Sprintf("health check got HTTP %d", resp.StatusCode)}
|
||||
}
|
||||
Reference in new issue
Block a user