Skip to content

Commit 3c4fe10

Browse files
committed
Proxy protocol support for SMTP and IMAP
1 parent f5def9c commit 3c4fe10

File tree

8 files changed

+238
-12
lines changed

8 files changed

+238
-12
lines changed

docs/reference/endpoints/imap.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,25 @@ tls cert.crt key.key {
4040

4141
See [TLS configuration / Server](/reference/tls/#server-side) for details.
4242

43+
**Syntax**: proxy_protocol _trusted ips..._ { ... } <br>
44+
**Default**: not enabled
45+
46+
Enable use of HAProxy PROXY protocol. Supports both v1 and v2 protocols.
47+
If a list of trusted IP addresses or subnets is provided, only connections
48+
from those will be trusted.
49+
50+
TLS for the channel between the proxies and maddy can be configured
51+
using a 'tls' directive:
52+
```
53+
proxy_protocol {
54+
trust 127.0.0.1 ::1 192.168.0.1/24
55+
tls &proxy_tls
56+
}
57+
```
58+
Note that the top-level 'tls' directive is not inherited here. If you
59+
need TLS on top of the PROXY protocol, securing the protocol header,
60+
you must declare TLS explicitly.
61+
4362
**Syntax**: io\_debug _boolean_ <br>
4463
**Default**: no
4564

docs/reference/endpoints/smtp.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,21 @@ tls cert.crt key.key {
5858

5959
See [TLS configuration / Server](/reference/tls/#server-side) for details.
6060

61+
**Syntax**: proxy_protocol _trusted ips..._ { ... } <br>
62+
**Default**: not enabled
63+
64+
Enable use of HAProxy PROXY protocol. Supports both v1 and v2 protocols.
65+
If a list of trusted IP addresses or subnets is provided, only connections
66+
from those will be trusted.
67+
68+
TLS for the channel between the proxies and maddy can be configured
69+
using a 'tls' directive:
70+
```
71+
proxy_protocol {
72+
trust 127.0.0.1 ::1 192.168.0.1/24
73+
tls &proxy_tls
74+
}
75+
```
6176

6277
**Syntax**: io\_debug _boolean_ <br>
6378
**Default**: no

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ require (
7373
github.com/aws/aws-sdk-go-v2/service/sts v1.18.3 // indirect
7474
github.com/aws/smithy-go v1.13.5 // indirect
7575
github.com/beorn7/perks v1.0.1 // indirect
76+
github.com/c0va23/go-proxyprotocol v0.9.1 // indirect
7677
github.com/cespare/xxhash/v2 v2.2.0 // indirect
7778
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
7879
github.com/digitalocean/godo v1.96.0 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,8 @@ github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLj
236236
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
237237
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
238238
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
239+
github.com/c0va23/go-proxyprotocol v0.9.1 h1:5BCkp0fDJOhzzH1lhjUgHhmZz9VvRMMif1U2D31hb34=
240+
github.com/c0va23/go-proxyprotocol v0.9.1/go.mod h1:TNjUV+llvk8TvWJxlPYAeAYZgSzT/iicNr3nWBWX320=
239241
github.com/caddyserver/certmagic v0.17.2 h1:o30seC1T/dBqBCNNGNHWwj2i5/I/FMjBbTAhjADP3nE=
240242
github.com/caddyserver/certmagic v0.17.2/go.mod h1:ouWUuC490GOLJzkyN35eXfV8bSbwMwSf4bdhkIxtdQE=
241243
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=

internal/endpoint/imap/imap.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,16 @@ import (
4444
"github.com/foxcpp/maddy/framework/module"
4545
"github.com/foxcpp/maddy/internal/auth"
4646
"github.com/foxcpp/maddy/internal/authz"
47+
"github.com/foxcpp/maddy/internal/proxy_protocol"
4748
"github.com/foxcpp/maddy/internal/updatepipe"
4849
)
4950

5051
type Endpoint struct {
51-
addrs []string
52-
serv *imapserver.Server
53-
listeners []net.Listener
54-
Store module.Storage
52+
addrs []string
53+
serv *imapserver.Server
54+
listeners []net.Listener
55+
proxyProtocol *proxy_protocol.ProxyProtocol
56+
Store module.Storage
5557

5658
tlsConfig *tls.Config
5759
listenersWg sync.WaitGroup
@@ -90,6 +92,7 @@ func (endp *Endpoint) Init(cfg *config.Map) error {
9092
})
9193
cfg.Custom("storage", false, true, nil, modconfig.StorageDirective, &endp.Store)
9294
cfg.Custom("tls", true, true, nil, tls2.TLSDirective, &endp.tlsConfig)
95+
cfg.Custom("proxy_protocol", false, false, nil, proxy_protocol.ProxyProtocolDirective, &endp.proxyProtocol)
9396
cfg.Bool("insecure_auth", false, false, &insecureAuth)
9497
cfg.Bool("io_debug", false, false, &ioDebug)
9598
cfg.Bool("io_errors", false, false, &ioErrors)
@@ -167,6 +170,10 @@ func (endp *Endpoint) setupListeners(addresses []config.Endpoint) error {
167170
l = tls.NewListener(l, endp.tlsConfig)
168171
}
169172

173+
if endp.proxyProtocol != nil {
174+
l = proxy_protocol.NewListener(l, endp.proxyProtocol, endp.Log)
175+
}
176+
170177
endp.listeners = append(endp.listeners, l)
171178

172179
endp.listenersWg.Add(1)

internal/endpoint/smtp/smtp.go

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,18 +46,20 @@ import (
4646
"github.com/foxcpp/maddy/internal/authz"
4747
"github.com/foxcpp/maddy/internal/limits"
4848
"github.com/foxcpp/maddy/internal/msgpipeline"
49+
"github.com/foxcpp/maddy/internal/proxy_protocol"
4950
"golang.org/x/net/idna"
5051
)
5152

5253
type Endpoint struct {
53-
saslAuth auth.SASLAuth
54-
serv *smtp.Server
55-
name string
56-
addrs []string
57-
listeners []net.Listener
58-
pipeline *msgpipeline.MsgPipeline
59-
resolver dns.Resolver
60-
limits *limits.Group
54+
saslAuth auth.SASLAuth
55+
serv *smtp.Server
56+
name string
57+
addrs []string
58+
listeners []net.Listener
59+
proxyProtocol *proxy_protocol.ProxyProtocol
60+
pipeline *msgpipeline.MsgPipeline
61+
resolver dns.Resolver
62+
limits *limits.Group
6163

6264
buffer func(r io.Reader) (buffer.Buffer, error)
6365

@@ -263,6 +265,7 @@ func (endp *Endpoint) setConfig(cfg *config.Map) error {
263265
return autoBufferMode(1*1024*1024 /* 1 MiB */, path), nil
264266
}, bufferModeDirective, &endp.buffer)
265267
cfg.Custom("tls", true, endp.name != "lmtp", nil, tls2.TLSDirective, &endp.serv.TLSConfig)
268+
cfg.Custom("proxy_protocol", false, false, nil, proxy_protocol.ProxyProtocolDirective, &endp.proxyProtocol)
266269
cfg.Bool("insecure_auth", endp.name == "lmtp", false, &endp.serv.AllowInsecureAuth)
267270
cfg.Int("smtp_max_line_length", false, false, 4000, &endp.serv.MaxLineLength)
268271
cfg.Bool("io_debug", false, false, &ioDebug)
@@ -350,6 +353,10 @@ func (endp *Endpoint) setupListeners(addresses []config.Endpoint) error {
350353
l = tls.NewListener(l, endp.serv.TLSConfig)
351354
}
352355

356+
if endp.proxyProtocol != nil {
357+
l = proxy_protocol.NewListener(l, endp.proxyProtocol, endp.Log)
358+
}
359+
353360
endp.listeners = append(endp.listeners, l)
354361

355362
endp.listenersWg.Add(1)
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package proxy_protocol
2+
3+
import (
4+
"crypto/tls"
5+
"net"
6+
"strings"
7+
8+
"github.com/c0va23/go-proxyprotocol"
9+
"github.com/foxcpp/maddy/framework/config"
10+
tls2 "github.com/foxcpp/maddy/framework/config/tls"
11+
"github.com/foxcpp/maddy/framework/log"
12+
)
13+
14+
type ProxyProtocol struct {
15+
trust []net.IPNet
16+
tlsConfig *tls.Config
17+
}
18+
19+
func ProxyProtocolDirective(_ *config.Map, node config.Node) (interface{}, error) {
20+
p := ProxyProtocol{}
21+
22+
childM := config.NewMap(nil, node)
23+
var trustList []string
24+
25+
childM.StringList("trust", false, false, nil, &trustList)
26+
childM.Custom("tls", true, false, nil, tls2.TLSDirective, &p.tlsConfig)
27+
28+
if _, err := childM.Process(); err != nil {
29+
return nil, err
30+
}
31+
32+
if len(node.Args) > 0 {
33+
if trustList == nil {
34+
trustList = make([]string, 0)
35+
}
36+
trustList = append(trustList, node.Args...)
37+
}
38+
39+
for _, trust := range trustList {
40+
if !strings.Contains(trust, "/") {
41+
trust += "/32"
42+
}
43+
_, ipNet, err := net.ParseCIDR(trust)
44+
if err != nil {
45+
return nil, err
46+
}
47+
p.trust = append(p.trust, *ipNet)
48+
}
49+
50+
return &p, nil
51+
}
52+
53+
func NewListener(inner net.Listener, p *ProxyProtocol, logger log.Logger) net.Listener {
54+
var listener net.Listener
55+
56+
sourceChecker := func(upstream net.Addr) (bool, error) {
57+
if tcpAddr, ok := upstream.(*net.TCPAddr); ok {
58+
if len(p.trust) == 0 {
59+
return true, nil
60+
}
61+
for _, trusted := range p.trust {
62+
if trusted.Contains(tcpAddr.IP) {
63+
return true, nil
64+
}
65+
}
66+
} else if _, ok := upstream.(*net.UnixAddr); ok {
67+
// UNIX local socket connection, always trusted
68+
return true, nil
69+
}
70+
71+
logger.Printf("proxy_protocol: connection from untrusted source %s", upstream)
72+
return false, nil
73+
}
74+
75+
listener = proxyprotocol.NewDefaultListener(inner).
76+
WithLogger(proxyprotocol.LoggerFunc(func(format string, v ...interface{}) {
77+
logger.Debugf("proxy_protocol: "+format, v...)
78+
})).
79+
WithSourceChecker(sourceChecker)
80+
81+
if p.tlsConfig != nil {
82+
listener = tls.NewListener(listener, p.tlsConfig)
83+
}
84+
85+
return listener
86+
}

tests/smtp_test.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ package tests_test
2323

2424
import (
2525
"errors"
26+
"fmt"
2627
"io/ioutil"
2728
"path/filepath"
2829
"strings"
@@ -68,6 +69,94 @@ func TestCheckRequireTLS(tt *testing.T) {
6869
conn.ExpectPattern("221 *")
6970
}
7071

72+
func TestProxyProtocolTrustedSource(tt *testing.T) {
73+
tt.Parallel()
74+
t := tests.NewT(tt)
75+
t.DNS(map[string]mockdns.Zone{
76+
"one.maddy.test.": {
77+
TXT: []string{"v=spf1 ip4:127.0.0.17 -all"},
78+
},
79+
})
80+
t.Port("smtp")
81+
t.Config(`
82+
smtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {
83+
hostname mx.maddy.test
84+
tls off
85+
86+
proxy_protocol {
87+
trust ` + tests.DefaultSourceIP.String() + ` ::1/128
88+
tls off
89+
}
90+
91+
defer_sender_reject no
92+
93+
check {
94+
spf {
95+
enforce_early yes
96+
fail_action reject
97+
}
98+
}
99+
100+
deliver_to dummy
101+
}
102+
`)
103+
t.Run(1)
104+
defer t.Close()
105+
106+
conn := t.Conn("smtp")
107+
defer conn.Close()
108+
conn.Writeln(fmt.Sprintf("PROXY TCP4 127.0.0.17 %s 12345 %d", tests.DefaultSourceIP.String(), t.Port("smtp")))
109+
conn.SMTPNegotation("localhost", nil, nil)
110+
conn.Writeln("MAIL FROM:<[email protected]>")
111+
conn.ExpectPattern("250 *")
112+
conn.Writeln("QUIT")
113+
conn.ExpectPattern("221 *")
114+
}
115+
116+
func TestProxyProtocolUntrustedSource(tt *testing.T) {
117+
tt.Parallel()
118+
t := tests.NewT(tt)
119+
t.DNS(map[string]mockdns.Zone{
120+
"one.maddy.test.": {
121+
TXT: []string{"v=spf1 ip4:127.0.0.17 -all"},
122+
},
123+
})
124+
t.Port("smtp")
125+
t.Config(`
126+
smtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {
127+
hostname mx.maddy.test
128+
tls off
129+
130+
proxy_protocol {
131+
trust fe80::bad/128
132+
tls off
133+
}
134+
135+
defer_sender_reject no
136+
137+
check {
138+
spf {
139+
enforce_early yes
140+
fail_action reject
141+
}
142+
}
143+
144+
deliver_to dummy
145+
}
146+
`)
147+
t.Run(1)
148+
defer t.Close()
149+
150+
conn := t.Conn("smtp")
151+
defer conn.Close()
152+
conn.Writeln(fmt.Sprintf("PROXY TCP4 127.0.0.17 %s 12345 %d", tests.DefaultSourceIP.String(), t.Port("smtp")))
153+
conn.SMTPNegotation("localhost", nil, nil)
154+
conn.Writeln("MAIL FROM:<[email protected]>")
155+
conn.ExpectPattern("550 *")
156+
conn.Writeln("QUIT")
157+
conn.ExpectPattern("221 *")
158+
}
159+
71160
func TestCheckSPF(tt *testing.T) {
72161
tt.Parallel()
73162
t := tests.NewT(tt)

0 commit comments

Comments
 (0)