Skip to content

Commit

Permalink
Add webhooks
Browse files Browse the repository at this point in the history
  • Loading branch information
rthellend committed Jan 3, 2025
1 parent 27363f6 commit b9ba6ad
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 16 deletions.
4 changes: 4 additions & 0 deletions proxy/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,10 @@ type ECH struct {
Interval time.Duration `yaml:"interval,omitempty"`
// The local endpoint where to publish the current ECH ConfigList.
Endpoint string `yaml:"endpoint,omitempty"`
// A list of WebHooks to call when the ECH config is updated. There is
// no payload other than the URLs themselves. The receipient should
// fetch the ECH endpoint (above) to get the current ConfigList.
WebHooks []string `yaml:"webhooks,omitempty"`
// The cloudflare DNS records to update when the ECH ConfigList changes.
Cloudflare []*Cloudflare `yaml:"cloudflare,omitempty"`
}
Expand Down
69 changes: 57 additions & 12 deletions proxy/ech.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import (
"io"
"net/http"
"slices"
"strconv"
"strings"
"time"

"github.com/c2FmZQ/ech"
Expand All @@ -47,7 +49,7 @@ type echKey struct {
PrivateKey []byte `json:"privateKey"`
}

func (p *Proxy) initECH() (retErr error) {
func (p *Proxy) rotateECH(forceCheck bool) (retErr error) {
if p.cfg.ECH == nil || p.cfg.ECH.PublicName == "" {
return nil
}
Expand All @@ -60,9 +62,7 @@ func (p *Proxy) initECH() (retErr error) {
}
defer commit(false, &retErr)

if len(echKeys) > 5 {
echKeys = echKeys[:5]
}
var changed bool
if len(echKeys) == 0 || echKeys[0].PublicName != p.cfg.ECH.PublicName || (p.cfg.ECH.Interval > 0 && time.Since(echKeys[0].CreationTime) > p.cfg.ECH.Interval) {
idExists := func(id uint8) bool {
return slices.IndexFunc(echKeys, func(k echKey) bool {
Expand Down Expand Up @@ -97,6 +97,10 @@ func (p *Proxy) initECH() (retErr error) {
return err
}
p.logErrorF("INF ECH ConfigList updated")
changed = true
}
if len(echKeys) > 5 {
echKeys = echKeys[:5]
}
p.echKeys = make([]tls.EncryptedClientHelloKey, 0, len(echKeys))
for i, k := range echKeys {
Expand All @@ -106,35 +110,76 @@ func (p *Proxy) initECH() (retErr error) {
SendAsRetry: i == 0,
})
}
configList, err := ech.ConfigList([]ech.Config{p.echKeys[0].Config})
p.echLastUpdate = echKeys[0].CreationTime
b, err := ech.ConfigList([]ech.Config{p.echKeys[0].Config})
if err != nil {
return err
}
if len(p.cfg.ECH.Cloudflare) > 0 {
configList := base64.StdEncoding.EncodeToString(b)
if (changed || forceCheck) && len(p.cfg.ECH.Cloudflare) > 0 {
go func() {
ctx := p.ctx
if ctx == nil {
ctx = context.Background()
}
ctx, cancel := context.WithTimeout(ctx, 5*time.Minute)
defer cancel()
cloudflare.UpdateECH(ctx, p.cfg.ECH.Cloudflare, base64.StdEncoding.EncodeToString(configList), p.logErrorF)
cloudflare.UpdateECH(ctx, p.cfg.ECH.Cloudflare, configList, p.logErrorF)
}()
}
if changed {
go func() {
ctx := p.ctx
if ctx == nil {
ctx = context.Background()
}
ctx, cancel := context.WithTimeout(ctx, 5*time.Minute)
defer cancel()

for _, wh := range p.cfg.ECH.WebHooks {
req, err := http.NewRequestWithContext(ctx, "POST", wh, nil)
if err != nil {
p.logErrorF("ERR ECH WebHook %q: %v", wh, err)
continue
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
p.logErrorF("ERR ECH WebHook %q: %v", wh, err)
continue
}
resp.Body.Close()
if resp.StatusCode != 200 {
p.logErrorF("ERR ECH WebHook %q: status code %d", wh, resp.StatusCode)
}
}
}()
}
return nil
}

func (p *Proxy) serveECHConfigList(w http.ResponseWriter, req *http.Request) {
if len(p.echKeys) == 0 {
p.mu.Lock()
lastUpdate := p.echLastUpdate
var config []byte
if len(p.echKeys) > 0 {
config = p.echKeys[0].Config
}
cacheTime := 6 * time.Hour
if p.cfg.ECH != nil && p.cfg.ECH.Interval > 0 {
cacheTime = min(cacheTime, p.cfg.ECH.Interval)
}
p.mu.Unlock()

if config == nil {
http.NotFound(w, req)
return
}
configList, err := ech.ConfigList([]ech.Config{p.echKeys[0].Config})
configList, err := ech.ConfigList([]ech.Config{config})
if err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
enc := base64.NewEncoder(base64.StdEncoding, w)
enc.Write(configList)
enc.Close()
w.Header().Set("Content-Type", "text/plain")
w.Header().Set("Cache-Control", "public, max-age="+strconv.Itoa(int(cacheTime.Seconds())))
http.ServeContent(w, req, "echConfigList", lastUpdate, strings.NewReader(base64.StdEncoding.EncodeToString(configList)))
}
9 changes: 5 additions & 4 deletions proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,8 @@ type Proxy struct {
eventsmu sync.Mutex
events map[string]int64

echKeys []tls.EncryptedClientHelloKey
echKeys []tls.EncryptedClientHelloKey
echLastUpdate time.Time
}

type beKey struct {
Expand Down Expand Up @@ -885,7 +886,7 @@ func (p *Proxy) Reconfigure(cfg *Config) error {
p.backends = backends
p.pkis = pkis
p.cfg = cfg
if err := p.initECH(); err != nil && err != storage.ErrRolledBack {
if err := p.rotateECH(true); err != nil && err != storage.ErrRolledBack {
return err
}
go p.reAuthorize()
Expand Down Expand Up @@ -977,9 +978,9 @@ func (p *Proxy) ctxWait(s *http.Server) {
}
p.Stop()
return
case <-time.After(60 * time.Minute):
case <-time.After(10 * time.Minute):
p.mu.Lock()
err := p.initECH()
err := p.rotateECH(false)
p.mu.Unlock()
if err != nil && err != storage.ErrRolledBack {
p.logErrorF("ERR ECH: %v", err)
Expand Down

0 comments on commit b9ba6ad

Please sign in to comment.