From 2a82ec655d795f98d52658e9b15de97054c320d9 Mon Sep 17 00:00:00 2001 From: DragonWork <97512+DragonWork@users.noreply.github.com> Date: Sun, 13 Oct 2024 17:55:22 +0000 Subject: [PATCH] Version 1.2.0 - Make TLSRPT option changeable without disrupting cache - Allow configuring Prefetch and TLSRPT option via environment variables (useful for Docker) - Update dependencies --- README.md | 3 +++ go.mod | 8 ++++---- go.sum | 16 ++++++++-------- src/mta-sts.go | 27 ++++++++++++--------------- src/prefetch.go | 4 ++-- src/server.go | 41 +++++++++++++++++++++++++++++------------ utils/Dockerfile | 30 ++++++++++++++++++------------ 7 files changed, 76 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index faf0f40..b00fc16 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,9 @@ Jump to *Postfix configuration* to integrate the socketmap server. To update the image, stop and remove the container, and run the above command again. +To disable prefetching, pass `-e TLSPOL_PREFETCH=0` to the above command. +To enable Postfix 3.10+ TLSRPT support, set `-e TLSPOL_TLSRPT=1`. + # Build from source ### Build a Docker container from source diff --git a/go.mod b/go.mod index a6d99a3..4680a98 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/Zuplu/postfix-tlspol -go 1.23.1 +go 1.23.2 require ( github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 @@ -13,8 +13,8 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect golang.org/x/mod v0.21.0 // indirect - golang.org/x/net v0.29.0 // indirect + golang.org/x/net v0.30.0 // indirect golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.25.0 // indirect - golang.org/x/tools v0.25.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/tools v0.26.0 // indirect ) diff --git a/go.sum b/go.sum index 4f87391..186c20f 100644 --- a/go.sum +++ b/go.sum @@ -18,16 +18,16 @@ github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= -golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= -golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= -golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= 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/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= diff --git a/src/mta-sts.go b/src/mta-sts.go index 8824983..4b83d29 100644 --- a/src/mta-sts.go +++ b/src/mta-sts.go @@ -48,13 +48,13 @@ func checkMtaStsRecord(domain string) (bool, error) { return false, nil } -func checkMtaSts(domain string) (string, uint32) { +func checkMtaSts(domain string) (string, string, uint32) { hasRecord, err := checkMtaStsRecord(domain) if err != nil { - return "TEMP", 0 + return "TEMP", "", 0 } if !hasRecord { - return "", 0 + return "", "", 0 } client := &http.Client{ @@ -75,39 +75,39 @@ func checkMtaSts(domain string) (string, uint32) { mtaSTSURL := "https://mta-sts." + domain + "/.well-known/mta-sts.txt" resp, err := client.Get(mtaSTSURL) if err != nil || resp.StatusCode != http.StatusOK { - return "", 0 + return "", "", 0 } defer resp.Body.Close() var mxServers []string mode := "" var maxAge uint32 = 0 - policy := "" + report := "" mxHosts := "" existingKeys := make(map[string]bool) scanner := bufio.NewScanner(resp.Body) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if !govalidator.IsPrintableASCII(line) && !govalidator.IsUTFLetterNumeric(line) { - return "", 0 // invalid policy, neither printable ASCII nor alphanumeric UTF-8 (latter is allowed in extended key/vals only) + return "", "", 0 // invalid policy, neither printable ASCII nor alphanumeric UTF-8 (latter is allowed in extended key/vals only) } if len(line) != len(govalidator.BlackList(line, "{}")) { continue // skip lines containing { or }, they are only allowed in extended key/vals, and we don't need them anyway } keyValPair := strings.SplitN(line, ":", 2) if len(keyValPair) != 2 { - return "", 0 // invalid policy + return "", "", 0 // invalid policy } key, val := strings.TrimSpace(keyValPair[0]), strings.TrimSpace(keyValPair[1]) if key != "mx" && existingKeys[key] { continue // only mx keys can be duplicated, others are ignored (as of [RFC 8641, 3.2]) } existingKeys[key] = true - policy = policy + " { policy_string = " + key + ": " + val + " }" + report = report + " { policy_string = " + key + ": " + val + " }" switch key { case "mx": if !govalidator.IsDNSName(strings.ReplaceAll(val, "*.", "")) { - return "", 0 // invalid policy + return "", "", 0 // invalid policy } mxHosts = mxHosts + " mx_host_pattern=" + val if strings.HasPrefix(val, "*.") { @@ -123,15 +123,12 @@ func checkMtaSts(domain string) (string, uint32) { } } } - policy = " policy_type=sts policy_domain=" + domain + fmt.Sprintf(" policy_ttl=%d", maxAge) + mxHosts + policy + report = "policy_type=sts policy_domain=" + domain + fmt.Sprintf(" policy_ttl=%d", maxAge) + mxHosts + report if mode == "enforce" { res := "secure match=" + strings.Join(mxServers, ":") + " servername=hostname" - if config.Server.TlsRpt { - res = res + policy - } - return res, maxAge + return res, report, maxAge } - return "", maxAge + return "", "", maxAge } diff --git a/src/prefetch.go b/src/prefetch.go index c8b5c27..12f7ccd 100644 --- a/src/prefetch.go +++ b/src/prefetch.go @@ -44,10 +44,10 @@ func prefetchCachedPolicies() { // Check if the original TTL is greater than the margin and within the prefetching range if cachedPolicy.Ttl >= PREFETCH_MARGIN && ttl < uint32(float64(cachedPolicy.Ttl)*0.05+PREFETCH_INTERVAL.Seconds()) { // Refresh the cached policy - refreshedResult, refreshedTtl := queryDomain(cachedPolicy.Domain, false) + refreshedResult, refreshedRpt, refreshedTtl := queryDomain(cachedPolicy.Domain, false) if refreshedResult != "" && refreshedResult != "TEMP" { fmt.Printf("Prefetched policy for %s: %s (cached for %ds)\n", cachedPolicy.Domain, refreshedResult, refreshedTtl) - cacheJsonSet(redisClient, key, CacheStruct{Domain: cachedPolicy.Domain, Result: refreshedResult, Ttl: refreshedTtl}) + cacheJsonSet(redisClient, key, CacheStruct{Domain: cachedPolicy.Domain, Result: refreshedResult, Report: refreshedRpt, Ttl: refreshedTtl}) } } }(key) diff --git a/src/server.go b/src/server.go index eed4ab0..42472b2 100644 --- a/src/server.go +++ b/src/server.go @@ -22,13 +22,14 @@ import ( ) const ( - VERSION = "1.1.4" - DB_SCHEMA = "2" + VERSION = "1.2.0" + DB_SCHEMA = "3" ) type CacheStruct struct { Domain string `json:"d"` Result string `json:"r"` + Report string `json:"p"` Ttl uint32 `json:"t"` } @@ -71,6 +72,15 @@ func main() { return } + envPrefetch, envExists := os.LookupEnv("TLSPOL_PREFETCH") + if envExists { + config.Server.Prefetch = envPrefetch == "1" + } + envTlsRpt, envExists := os.LookupEnv("TLSPOL_TLSRPT") + if envExists { + config.Server.TlsRpt = envTlsRpt == "1" + } + if !config.Redis.Disable { // Setup redis client for cache redisClient = redis.NewClient(&redis.Options{ @@ -170,13 +180,16 @@ func handleConnection(conn net.Conn) { conn.Write([]byte("5:TEMP ,")) } else { fmt.Printf("Evaluated policy for %s: %s (from cache, %ds remaining)\n", domain, cache.Result, ttl) + if config.Server.TlsRpt { + cache.Result = cache.Result + " " + cache.Report + } conn.Write([]byte(fmt.Sprintf("%d:OK %s,", len(cache.Result)+3, cache.Result))) } return } } - result, resultTtl := queryDomain(domain, true) + result, resultRpt, resultTtl := queryDomain(domain, true) if result == "" { fmt.Printf("No policy found for %s (cached for %ds)\n", domain, resultTtl) @@ -186,30 +199,34 @@ func handleConnection(conn net.Conn) { conn.Write([]byte("5:TEMP ,")) } else { fmt.Printf("Evaluated policy for %s: %s (cached for %ds)\n", domain, result, resultTtl) - conn.Write([]byte(fmt.Sprintf("%d:OK %s,", len(result)+3, result))) + res := result + if config.Server.TlsRpt { + res = res + " " + resultRpt + } + conn.Write([]byte(fmt.Sprintf("%d:OK %s,", len(res)+3, res))) } if !config.Redis.Disable { - cacheJsonSet(redisClient, cacheKey, CacheStruct{Domain: domain, Result: result, Ttl: resultTtl}) + cacheJsonSet(redisClient, cacheKey, CacheStruct{Domain: domain, Result: result, Report: resultRpt, Ttl: resultTtl}) } } -func queryDomain(domain string, parallelize bool) (string, uint32) { +func queryDomain(domain string, parallelize bool) (string, string, uint32) { result := "" + resultRpt := "" var resultTtl uint32 = CACHE_NOTFOUND_TTL var mutex sync.Mutex var wg sync.WaitGroup // DANE query - var daneTtl uint32 = 0 wg.Add(1) go func() { defer wg.Done() - var danePol string - danePol, daneTtl = checkDane(domain) + danePol, daneTtl := checkDane(domain) mutex.Lock() if danePol != "" { result = danePol + resultRpt = "" resultTtl = daneTtl } mutex.Unlock() @@ -220,7 +237,6 @@ func queryDomain(domain string, parallelize bool) (string, uint32) { } // MTA-STS query - var stsTtl uint32 = 0 wg.Add(1) go func() { defer wg.Done() @@ -228,10 +244,11 @@ func queryDomain(domain string, parallelize bool) (string, uint32) { return } var stsPol string - stsPol, stsTtl = checkMtaSts(domain) + stsPol, stsRpt, stsTtl := checkMtaSts(domain) mutex.Lock() if stsPol != "" && result == "" { result = stsPol + resultRpt = stsRpt resultTtl = stsTtl } mutex.Unlock() @@ -246,7 +263,7 @@ func queryDomain(domain string, parallelize bool) (string, uint32) { resultTtl = CACHE_MIN_TTL } - return result, resultTtl + return result, resultRpt, resultTtl } func cacheJsonGet(redisClient *redis.Client, cacheKey string) (CacheStruct, uint32, error) { diff --git a/utils/Dockerfile b/utils/Dockerfile index 920495c..dcbae65 100644 --- a/utils/Dockerfile +++ b/utils/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.23.1-alpine3.20 AS workspace +FROM golang:1-alpine AS workspace # Create a non-root user RUN addgroup -g 1000 default \ @@ -7,31 +7,37 @@ RUN addgroup -g 1000 default \ # Install unbound and redis RUN apk add --no-cache unbound redis -RUN chown default:default /var/lib/redis \ - && chmod 4755 /usr/sbin/unbound - # Copy postfix-tlspol -COPY . /postfix-tlspol +COPY --chown=default:default . /build # Build postfix-tlspol and remove Go toolchain -RUN /postfix-tlspol/build.sh build-only \ +RUN /build/build.sh build-only \ && go clean -cache -modcache \ - && rm -rf /usr/local/go + && rm -rf /usr/local/go \ + && rm -rf /go + +# Create data dir +RUN mkdir -p /data \ + && chown default:default /data -# Configure postfix-tlspol -RUN sed -i -e "s/127\.0\.0\.1:8642/0\.0\.0\.0:8642/" -e "s/prefetch: no/prefetch: yes/" /postfix-tlspol/config.yaml \ - && chown -R default:default /postfix-tlspol +# Setup postfix-tlspol +RUN sed -i -e "s/127\.0\.0\.1:8642/0\.0\.0\.0:8642/" -e "s/prefetch: no/prefetch: yes/" /build/config.yaml \ + && chown -R default:default /build \ + && mv /build/postfix-tlspol / \ + && mv /build/config.yaml /data/ \ + && rm -rf /build # Setup unbound RUN chown -R default:default /usr/share/dnssec-root \ + && chmod 4755 /usr/sbin/unbound \ && echo -e "server:\n username: default\n chroot: \"\"\n do-daemonize: no\n use-syslog: no\n verbosity: 1\n interface: 127.0.0.53\n auto-trust-anchor-file: /usr/share/dnssec-root/trusted-key.key\n cache-min-ttl: 10\n cache-max-ttl: 180\n serve-original-ttl: yes" > /etc/unbound/unbound.conf \ && chown -R default:default /etc/unbound # Setup redis -RUN echo -e "bind 127.0.0.1 -::1\nport 6379\ndaemonize no\nlogfile \"\"\nloglevel notice\ndbfilename dump.rdb\ndir /var/lib/redis" > /etc/redis.conf +RUN echo -e "bind 127.0.0.1 -::1\nport 6379\ndaemonize no\nlogfile \"\"\nloglevel notice\ndbfilename cache.rdb\ndir /data" > /etc/redis.conf # Setup entrypoint -RUN echo -e "#!/bin/sh\n( cd /var/lib/redis ; /usr/bin/redis-server /etc/redis.conf ) &\n( cd /etc/unbound ; /usr/sbin/unbound -c /etc/unbound/unbound.conf ) &\nexec /postfix-tlspol/postfix-tlspol /postfix-tlspol/config.yaml" > /entrypoint.sh && chmod +x /entrypoint.sh +RUN echo -e "#!/bin/sh\necho 1 > /proc/sys/vm/overcommit_memory\n( cd /data ; /usr/bin/redis-server /etc/redis.conf ) &\n( cd /etc/unbound ; /usr/sbin/unbound -c /etc/unbound/unbound.conf ) &\nexec /postfix-tlspol /data/config.yaml" > /entrypoint.sh && chmod +x /entrypoint.sh # Squash layers FROM scratch