Skip to content

Commit

Permalink
Version 1.2.0
Browse files Browse the repository at this point in the history
- Make TLSRPT option changeable without disrupting cache
- Allow configuring Prefetch and TLSRPT option via environment variables (useful for Docker)
- Update dependencies
  • Loading branch information
DragonWork committed Oct 13, 2024
1 parent 7613a5e commit 2a82ec6
Show file tree
Hide file tree
Showing 7 changed files with 76 additions and 53 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
)
16 changes: 8 additions & 8 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
27 changes: 12 additions & 15 deletions src/mta-sts.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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, "*.") {
Expand All @@ -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
}
4 changes: 2 additions & 2 deletions src/prefetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
41 changes: 29 additions & 12 deletions src/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}

Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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)
Expand All @@ -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()
Expand All @@ -220,18 +237,18 @@ func queryDomain(domain string, parallelize bool) (string, uint32) {
}

// MTA-STS query
var stsTtl uint32 = 0
wg.Add(1)
go func() {
defer wg.Done()
if result != "" {
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()
Expand All @@ -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) {
Expand Down
30 changes: 18 additions & 12 deletions utils/Dockerfile
Original file line number Diff line number Diff line change
@@ -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 \
Expand All @@ -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
Expand Down

0 comments on commit 2a82ec6

Please sign in to comment.