1package mox
2
3import (
4 "bytes"
5 "context"
6 "crypto"
7 "crypto/ed25519"
8 cryptorand "crypto/rand"
9 "crypto/rsa"
10 "crypto/sha256"
11 "crypto/x509"
12 "encoding/pem"
13 "fmt"
14 "log/slog"
15 "net"
16 "net/url"
17 "os"
18 "path/filepath"
19 "sort"
20 "strings"
21 "time"
22
23 "golang.org/x/exp/maps"
24
25 "github.com/mjl-/adns"
26
27 "github.com/mjl-/mox/config"
28 "github.com/mjl-/mox/dkim"
29 "github.com/mjl-/mox/dmarc"
30 "github.com/mjl-/mox/dns"
31 "github.com/mjl-/mox/junk"
32 "github.com/mjl-/mox/mtasts"
33 "github.com/mjl-/mox/smtp"
34 "github.com/mjl-/mox/tlsrpt"
35)
36
37// TXTStrings returns a TXT record value as one or more quoted strings, each max
38// 100 characters. In case of multiple strings, a multi-line record is returned.
39func TXTStrings(s string) string {
40 if len(s) <= 100 {
41 return `"` + s + `"`
42 }
43
44 r := "(\n"
45 for len(s) > 0 {
46 n := len(s)
47 if n > 100 {
48 n = 100
49 }
50 if r != "" {
51 r += " "
52 }
53 r += "\t\t\"" + s[:n] + "\"\n"
54 s = s[n:]
55 }
56 r += "\t)"
57 return r
58}
59
60// MakeDKIMEd25519Key returns a PEM buffer containing an ed25519 key for use
61// with DKIM.
62// selector and domain can be empty. If not, they are used in the note.
63func MakeDKIMEd25519Key(selector, domain dns.Domain) ([]byte, error) {
64 _, privKey, err := ed25519.GenerateKey(cryptorand.Reader)
65 if err != nil {
66 return nil, fmt.Errorf("generating key: %w", err)
67 }
68
69 pkcs8, err := x509.MarshalPKCS8PrivateKey(privKey)
70 if err != nil {
71 return nil, fmt.Errorf("marshal key: %w", err)
72 }
73
74 block := &pem.Block{
75 Type: "PRIVATE KEY",
76 Headers: map[string]string{
77 "Note": dkimKeyNote("ed25519", selector, domain),
78 },
79 Bytes: pkcs8,
80 }
81 b := &bytes.Buffer{}
82 if err := pem.Encode(b, block); err != nil {
83 return nil, fmt.Errorf("encoding pem: %w", err)
84 }
85 return b.Bytes(), nil
86}
87
88func dkimKeyNote(kind string, selector, domain dns.Domain) string {
89 s := kind + " dkim private key"
90 var zero dns.Domain
91 if selector != zero && domain != zero {
92 s += fmt.Sprintf(" for %s._domainkey.%s", selector.ASCII, domain.ASCII)
93 }
94 s += fmt.Sprintf(", generated by mox on %s", time.Now().Format(time.RFC3339))
95 return s
96}
97
98// MakeDKIMEd25519Key returns a PEM buffer containing an rsa key for use with
99// DKIM.
100// selector and domain can be empty. If not, they are used in the note.
101func MakeDKIMRSAKey(selector, domain dns.Domain) ([]byte, error) {
102 // 2048 bits seems reasonable in 2022, 1024 is on the low side, larger
103 // keys may not fit in UDP DNS response.
104 privKey, err := rsa.GenerateKey(cryptorand.Reader, 2048)
105 if err != nil {
106 return nil, fmt.Errorf("generating key: %w", err)
107 }
108
109 pkcs8, err := x509.MarshalPKCS8PrivateKey(privKey)
110 if err != nil {
111 return nil, fmt.Errorf("marshal key: %w", err)
112 }
113
114 block := &pem.Block{
115 Type: "PRIVATE KEY",
116 Headers: map[string]string{
117 "Note": dkimKeyNote("rsa-2048", selector, domain),
118 },
119 Bytes: pkcs8,
120 }
121 b := &bytes.Buffer{}
122 if err := pem.Encode(b, block); err != nil {
123 return nil, fmt.Errorf("encoding pem: %w", err)
124 }
125 return b.Bytes(), nil
126}
127
128// MakeAccountConfig returns a new account configuration for an email address.
129func MakeAccountConfig(addr smtp.Address) config.Account {
130 account := config.Account{
131 Domain: addr.Domain.Name(),
132 Destinations: map[string]config.Destination{
133 addr.String(): {},
134 },
135 RejectsMailbox: "Rejects",
136 JunkFilter: &config.JunkFilter{
137 Threshold: 0.95,
138 Params: junk.Params{
139 Onegrams: true,
140 MaxPower: .01,
141 TopWords: 10,
142 IgnoreWords: .1,
143 RareWords: 2,
144 },
145 },
146 }
147 account.AutomaticJunkFlags.Enabled = true
148 account.AutomaticJunkFlags.JunkMailboxRegexp = "^(junk|spam)"
149 account.AutomaticJunkFlags.NeutralMailboxRegexp = "^(inbox|neutral|postmaster|dmarc|tlsrpt|rejects)"
150 account.SubjectPass.Period = 12 * time.Hour
151 return account
152}
153
154// MakeDomainConfig makes a new config for a domain, creating DKIM keys, using
155// accountName for DMARC and TLS reports.
156func MakeDomainConfig(ctx context.Context, domain, hostname dns.Domain, accountName string, withMTASTS bool) (config.Domain, []string, error) {
157 log := pkglog.WithContext(ctx)
158
159 now := time.Now()
160 year := now.Format("2006")
161 timestamp := now.Format("20060102T150405")
162
163 var paths []string
164 defer func() {
165 for _, p := range paths {
166 err := os.Remove(p)
167 log.Check(err, "removing path for domain config", slog.String("path", p))
168 }
169 }()
170
171 writeFile := func(path string, data []byte) error {
172 os.MkdirAll(filepath.Dir(path), 0770)
173
174 f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0660)
175 if err != nil {
176 return fmt.Errorf("creating file %s: %s", path, err)
177 }
178 defer func() {
179 if f != nil {
180 err := f.Close()
181 log.Check(err, "closing file after error")
182 err = os.Remove(path)
183 log.Check(err, "removing file after error", slog.String("path", path))
184 }
185 }()
186 if _, err := f.Write(data); err != nil {
187 return fmt.Errorf("writing file %s: %s", path, err)
188 }
189 if err := f.Close(); err != nil {
190 return fmt.Errorf("close file: %v", err)
191 }
192 f = nil
193 return nil
194 }
195
196 confDKIM := config.DKIM{
197 Selectors: map[string]config.Selector{},
198 }
199
200 addSelector := func(kind, name string, privKey []byte) error {
201 record := fmt.Sprintf("%s._domainkey.%s", name, domain.ASCII)
202 keyPath := filepath.Join("dkim", fmt.Sprintf("%s.%s.%s.privatekey.pkcs8.pem", record, timestamp, kind))
203 p := configDirPath(ConfigDynamicPath, keyPath)
204 if err := writeFile(p, privKey); err != nil {
205 return err
206 }
207 paths = append(paths, p)
208 confDKIM.Selectors[name] = config.Selector{
209 // Example from RFC has 5 day between signing and expiration. ../rfc/6376:1393
210 // Expiration is not intended as antireplay defense, but it may help. ../rfc/6376:1340
211 // Messages in the wild have been observed with 2 hours and 1 year expiration.
212 Expiration: "72h",
213 PrivateKeyFile: keyPath,
214 }
215 return nil
216 }
217
218 addEd25519 := func(name string) error {
219 key, err := MakeDKIMEd25519Key(dns.Domain{ASCII: name}, domain)
220 if err != nil {
221 return fmt.Errorf("making dkim ed25519 private key: %s", err)
222 }
223 return addSelector("ed25519", name, key)
224 }
225
226 addRSA := func(name string) error {
227 key, err := MakeDKIMRSAKey(dns.Domain{ASCII: name}, domain)
228 if err != nil {
229 return fmt.Errorf("making dkim rsa private key: %s", err)
230 }
231 return addSelector("rsa2048", name, key)
232 }
233
234 if err := addEd25519(year + "a"); err != nil {
235 return config.Domain{}, nil, err
236 }
237 if err := addRSA(year + "b"); err != nil {
238 return config.Domain{}, nil, err
239 }
240 if err := addEd25519(year + "c"); err != nil {
241 return config.Domain{}, nil, err
242 }
243 if err := addRSA(year + "d"); err != nil {
244 return config.Domain{}, nil, err
245 }
246
247 // We sign with the first two. In case they are misused, the switch to the other
248 // keys is easy, just change the config. Operators should make the public key field
249 // of the misused keys empty in the DNS records to disable the misused keys.
250 confDKIM.Sign = []string{year + "a", year + "b"}
251
252 confDomain := config.Domain{
253 ClientSettingsDomain: "mail." + domain.Name(),
254 LocalpartCatchallSeparator: "+",
255 DKIM: confDKIM,
256 DMARC: &config.DMARC{
257 Account: accountName,
258 Localpart: "dmarc-reports",
259 Mailbox: "DMARC",
260 },
261 TLSRPT: &config.TLSRPT{
262 Account: accountName,
263 Localpart: "tls-reports",
264 Mailbox: "TLSRPT",
265 },
266 }
267
268 if withMTASTS {
269 confDomain.MTASTS = &config.MTASTS{
270 PolicyID: time.Now().UTC().Format("20060102T150405"),
271 Mode: mtasts.ModeEnforce,
272 // We start out with 24 hour, and warn in the admin interface that users should
273 // increase it to weeks once the setup works.
274 MaxAge: 24 * time.Hour,
275 MX: []string{hostname.ASCII},
276 }
277 }
278
279 rpaths := paths
280 paths = nil
281
282 return confDomain, rpaths, nil
283}
284
285// DomainAdd adds the domain to the domains config, rewriting domains.conf and
286// marking it loaded.
287//
288// accountName is used for DMARC/TLS report and potentially for the postmaster address.
289// If the account does not exist, it is created with localpart. Localpart must be
290// set only if the account does not yet exist.
291func DomainAdd(ctx context.Context, domain dns.Domain, accountName string, localpart smtp.Localpart) (rerr error) {
292 log := pkglog.WithContext(ctx)
293 defer func() {
294 if rerr != nil {
295 log.Errorx("adding domain", rerr,
296 slog.Any("domain", domain),
297 slog.String("account", accountName),
298 slog.Any("localpart", localpart))
299 }
300 }()
301
302 Conf.dynamicMutex.Lock()
303 defer Conf.dynamicMutex.Unlock()
304
305 c := Conf.Dynamic
306 if _, ok := c.Domains[domain.Name()]; ok {
307 return fmt.Errorf("domain already present")
308 }
309
310 // Compose new config without modifying existing data structures. If we fail, we
311 // leave no trace.
312 nc := c
313 nc.Domains = map[string]config.Domain{}
314 for name, d := range c.Domains {
315 nc.Domains[name] = d
316 }
317
318 // Only enable mta-sts for domain if there is a listener with mta-sts.
319 var withMTASTS bool
320 for _, l := range Conf.Static.Listeners {
321 if l.MTASTSHTTPS.Enabled {
322 withMTASTS = true
323 break
324 }
325 }
326
327 confDomain, cleanupFiles, err := MakeDomainConfig(ctx, domain, Conf.Static.HostnameDomain, accountName, withMTASTS)
328 if err != nil {
329 return fmt.Errorf("preparing domain config: %v", err)
330 }
331 defer func() {
332 for _, f := range cleanupFiles {
333 err := os.Remove(f)
334 log.Check(err, "cleaning up file after error", slog.String("path", f))
335 }
336 }()
337
338 if _, ok := c.Accounts[accountName]; ok && localpart != "" {
339 return fmt.Errorf("account already exists (leave localpart empty when using an existing account)")
340 } else if !ok && localpart == "" {
341 return fmt.Errorf("account does not yet exist (specify a localpart)")
342 } else if accountName == "" {
343 return fmt.Errorf("account name is empty")
344 } else if !ok {
345 nc.Accounts[accountName] = MakeAccountConfig(smtp.Address{Localpart: localpart, Domain: domain})
346 } else if accountName != Conf.Static.Postmaster.Account {
347 nacc := nc.Accounts[accountName]
348 nd := map[string]config.Destination{}
349 for k, v := range nacc.Destinations {
350 nd[k] = v
351 }
352 pmaddr := smtp.Address{Localpart: "postmaster", Domain: domain}
353 nd[pmaddr.String()] = config.Destination{}
354 nacc.Destinations = nd
355 nc.Accounts[accountName] = nacc
356 }
357
358 nc.Domains[domain.Name()] = confDomain
359
360 if err := writeDynamic(ctx, log, nc); err != nil {
361 return fmt.Errorf("writing domains.conf: %v", err)
362 }
363 log.Info("domain added", slog.Any("domain", domain))
364 cleanupFiles = nil // All good, don't cleanup.
365 return nil
366}
367
368// DomainRemove removes domain from the config, rewriting domains.conf.
369//
370// No accounts are removed, also not when they still reference this domain.
371func DomainRemove(ctx context.Context, domain dns.Domain) (rerr error) {
372 log := pkglog.WithContext(ctx)
373 defer func() {
374 if rerr != nil {
375 log.Errorx("removing domain", rerr, slog.Any("domain", domain))
376 }
377 }()
378
379 Conf.dynamicMutex.Lock()
380 defer Conf.dynamicMutex.Unlock()
381
382 c := Conf.Dynamic
383 domConf, ok := c.Domains[domain.Name()]
384 if !ok {
385 return fmt.Errorf("domain does not exist")
386 }
387
388 // Compose new config without modifying existing data structures. If we fail, we
389 // leave no trace.
390 nc := c
391 nc.Domains = map[string]config.Domain{}
392 s := domain.Name()
393 for name, d := range c.Domains {
394 if name != s {
395 nc.Domains[name] = d
396 }
397 }
398
399 if err := writeDynamic(ctx, log, nc); err != nil {
400 return fmt.Errorf("writing domains.conf: %v", err)
401 }
402
403 // Move away any DKIM private keys to a subdirectory "old". But only if
404 // they are not in use by other domains.
405 usedKeyPaths := map[string]bool{}
406 for _, dc := range nc.Domains {
407 for _, sel := range dc.DKIM.Selectors {
408 usedKeyPaths[filepath.Clean(sel.PrivateKeyFile)] = true
409 }
410 }
411 for _, sel := range domConf.DKIM.Selectors {
412 if sel.PrivateKeyFile == "" || usedKeyPaths[filepath.Clean(sel.PrivateKeyFile)] {
413 continue
414 }
415 src := ConfigDirPath(sel.PrivateKeyFile)
416 dst := ConfigDirPath(filepath.Join(filepath.Dir(sel.PrivateKeyFile), "old", filepath.Base(sel.PrivateKeyFile)))
417 _, err := os.Stat(dst)
418 if err == nil {
419 err = fmt.Errorf("destination already exists")
420 } else if os.IsNotExist(err) {
421 os.MkdirAll(filepath.Dir(dst), 0770)
422 err = os.Rename(src, dst)
423 }
424 if err != nil {
425 log.Errorx("renaming dkim private key file for removed domain", err, slog.String("src", src), slog.String("dst", dst))
426 }
427 }
428
429 log.Info("domain removed", slog.Any("domain", domain))
430 return nil
431}
432
433func WebserverConfigSet(ctx context.Context, domainRedirects map[string]string, webhandlers []config.WebHandler) (rerr error) {
434 log := pkglog.WithContext(ctx)
435 defer func() {
436 if rerr != nil {
437 log.Errorx("saving webserver config", rerr)
438 }
439 }()
440
441 Conf.dynamicMutex.Lock()
442 defer Conf.dynamicMutex.Unlock()
443
444 // Compose new config without modifying existing data structures. If we fail, we
445 // leave no trace.
446 nc := Conf.Dynamic
447 nc.WebDomainRedirects = domainRedirects
448 nc.WebHandlers = webhandlers
449
450 if err := writeDynamic(ctx, log, nc); err != nil {
451 return fmt.Errorf("writing domains.conf: %v", err)
452 }
453
454 log.Info("webserver config saved")
455 return nil
456}
457
458// todo: find a way to automatically create the dns records as it would greatly simplify setting up email for a domain. we could also dynamically make changes, e.g. providing grace periods after disabling a dkim key, only automatically removing the dkim dns key after a few days. but this requires some kind of api and authentication to the dns server. there doesn't appear to be a single commonly used api for dns management. each of the numerous cloud providers have their own APIs and rather large SKDs to use them. we don't want to link all of them in.
459
460// DomainRecords returns text lines describing DNS records required for configuring
461// a domain.
462//
463// If certIssuerDomainName is set, CAA records to limit TLS certificate issuance to
464// that caID will be suggested. If acmeAccountURI is also set, CAA records also
465// restricting issuance to that account ID will be suggested.
466func DomainRecords(domConf config.Domain, domain dns.Domain, hasDNSSEC bool, certIssuerDomainName, acmeAccountURI string) ([]string, error) {
467 d := domain.ASCII
468 h := Conf.Static.HostnameDomain.ASCII
469
470 // The first line with ";" is used by ../testdata/integration/moxacmepebble.sh and
471 // ../testdata/integration/moxmail2.sh for selecting DNS records
472 records := []string{
473 "; Time To Live of 5 minutes, may be recognized if importing as a zone file.",
474 "; Once your setup is working, you may want to increase the TTL.",
475 "$TTL 300",
476 "",
477 }
478
479 if public, ok := Conf.Static.Listeners["public"]; ok && public.TLS != nil && (len(public.TLS.HostPrivateRSA2048Keys) > 0 || len(public.TLS.HostPrivateECDSAP256Keys) > 0) {
480 records = append(records,
481 `; DANE: These records indicate that a remote mail server trying to deliver email`,
482 `; with SMTP (TCP port 25) must verify the TLS certificate with DANE-EE (3), based`,
483 `; on the certificate public key ("SPKI", 1) that is SHA2-256-hashed (1) to the`,
484 `; hexadecimal hash. DANE-EE verification means only the certificate or public`,
485 `; key is verified, not whether the certificate is signed by a (centralized)`,
486 `; certificate authority (CA), is expired, or matches the host name.`,
487 `;`,
488 `; NOTE: Create the records below only once: They are for the machine, and apply`,
489 `; to all hosted domains.`,
490 )
491 if !hasDNSSEC {
492 records = append(records,
493 ";",
494 "; WARNING: Domain does not appear to be DNSSEC-signed. To enable DANE, first",
495 "; enable DNSSEC on your domain, then add the TLSA records. Records below have been",
496 "; commented out.",
497 )
498 }
499 addTLSA := func(privKey crypto.Signer) error {
500 spkiBuf, err := x509.MarshalPKIXPublicKey(privKey.Public())
501 if err != nil {
502 return fmt.Errorf("marshal SubjectPublicKeyInfo for DANE record: %v", err)
503 }
504 sum := sha256.Sum256(spkiBuf)
505 tlsaRecord := adns.TLSA{
506 Usage: adns.TLSAUsageDANEEE,
507 Selector: adns.TLSASelectorSPKI,
508 MatchType: adns.TLSAMatchTypeSHA256,
509 CertAssoc: sum[:],
510 }
511 var s string
512 if hasDNSSEC {
513 s = fmt.Sprintf("_25._tcp.%-*s TLSA %s", 20+len(d)-len("_25._tcp."), h+".", tlsaRecord.Record())
514 } else {
515 s = fmt.Sprintf(";; _25._tcp.%-*s TLSA %s", 20+len(d)-len(";; _25._tcp."), h+".", tlsaRecord.Record())
516 }
517 records = append(records, s)
518 return nil
519 }
520 for _, privKey := range public.TLS.HostPrivateECDSAP256Keys {
521 if err := addTLSA(privKey); err != nil {
522 return nil, err
523 }
524 }
525 for _, privKey := range public.TLS.HostPrivateRSA2048Keys {
526 if err := addTLSA(privKey); err != nil {
527 return nil, err
528 }
529 }
530 records = append(records, "")
531 }
532
533 if d != h {
534 records = append(records,
535 "; For the machine, only needs to be created once, for the first domain added:",
536 "; ",
537 "; SPF-allow host for itself, resulting in relaxed DMARC pass for (postmaster)",
538 "; messages (DSNs) sent from host:",
539 fmt.Sprintf(`%-*s TXT "v=spf1 a -all"`, 20+len(d), h+"."), // ../rfc/7208:2263 ../rfc/7208:2287
540 "",
541 )
542 }
543 if d != h && Conf.Static.HostTLSRPT.ParsedLocalpart != "" {
544 uri := url.URL{
545 Scheme: "mailto",
546 Opaque: smtp.NewAddress(Conf.Static.HostTLSRPT.ParsedLocalpart, Conf.Static.HostnameDomain).Pack(false),
547 }
548 tlsrptr := tlsrpt.Record{Version: "TLSRPTv1", RUAs: [][]tlsrpt.RUA{{tlsrpt.RUA(uri.String())}}}
549 records = append(records,
550 "; For the machine, only needs to be created once, for the first domain added:",
551 "; ",
552 "; Request reporting about success/failures of TLS connections to (MX) host, for DANE.",
553 fmt.Sprintf(`_smtp._tls.%-*s TXT "%s"`, 20+len(d)-len("_smtp._tls."), h+".", tlsrptr.String()),
554 "",
555 )
556 }
557
558 records = append(records,
559 "; Deliver email for the domain to this host.",
560 fmt.Sprintf("%s. MX 10 %s.", d, h),
561 "",
562
563 "; Outgoing messages will be signed with the first two DKIM keys. The other two",
564 "; configured for backup, switching to them is just a config change.",
565 )
566 var selectors []string
567 for name := range domConf.DKIM.Selectors {
568 selectors = append(selectors, name)
569 }
570 sort.Slice(selectors, func(i, j int) bool {
571 return selectors[i] < selectors[j]
572 })
573 for _, name := range selectors {
574 sel := domConf.DKIM.Selectors[name]
575 dkimr := dkim.Record{
576 Version: "DKIM1",
577 Hashes: []string{"sha256"},
578 PublicKey: sel.Key.Public(),
579 }
580 if _, ok := sel.Key.(ed25519.PrivateKey); ok {
581 dkimr.Key = "ed25519"
582 } else if _, ok := sel.Key.(*rsa.PrivateKey); !ok {
583 return nil, fmt.Errorf("unrecognized private key for DKIM selector %q: %T", name, sel.Key)
584 }
585 txt, err := dkimr.Record()
586 if err != nil {
587 return nil, fmt.Errorf("making DKIM DNS TXT record: %v", err)
588 }
589
590 if len(txt) > 100 {
591 records = append(records,
592 "; NOTE: The following strings must be added to DNS as single record.",
593 )
594 }
595 s := fmt.Sprintf("%s._domainkey.%s. TXT %s", name, d, TXTStrings(txt))
596 records = append(records, s)
597
598 }
599 dmarcr := dmarc.DefaultRecord
600 dmarcr.Policy = "reject"
601 if domConf.DMARC != nil {
602 uri := url.URL{
603 Scheme: "mailto",
604 Opaque: smtp.NewAddress(domConf.DMARC.ParsedLocalpart, domConf.DMARC.DNSDomain).Pack(false),
605 }
606 dmarcr.AggregateReportAddresses = []dmarc.URI{
607 {Address: uri.String(), MaxSize: 10, Unit: "m"},
608 }
609 }
610 records = append(records,
611 "",
612
613 "; Specify the MX host is allowed to send for our domain and for itself (for DSNs).",
614 "; ~all means softfail for anything else, which is done instead of -all to prevent older",
615 "; mail servers from rejecting the message because they never get to looking for a dkim/dmarc pass.",
616 fmt.Sprintf(`%s. TXT "v=spf1 mx ~all"`, d),
617 "",
618
619 "; Emails that fail the DMARC check (without aligned DKIM and without aligned SPF)",
620 "; should be rejected, and request reports. If you email through mailing lists that",
621 "; strip DKIM-Signature headers and don't rewrite the From header, you may want to",
622 "; set the policy to p=none.",
623 fmt.Sprintf(`_dmarc.%s. TXT "%s"`, d, dmarcr.String()),
624 "",
625 )
626
627 if sts := domConf.MTASTS; sts != nil {
628 records = append(records,
629 "; Remote servers can use MTA-STS to verify our TLS certificate with the",
630 "; WebPKI pool of CA's (certificate authorities) when delivering over SMTP with",
631 "; STARTTLSTLS.",
632 fmt.Sprintf(`mta-sts.%s. CNAME %s.`, d, h),
633 fmt.Sprintf(`_mta-sts.%s. TXT "v=STSv1; id=%s"`, d, sts.PolicyID),
634 "",
635 )
636 } else {
637 records = append(records,
638 "; Note: No MTA-STS to indicate TLS should be used. Either because disabled for the",
639 "; domain or because mox.conf does not have a listener with MTA-STS configured.",
640 "",
641 )
642 }
643
644 if domConf.TLSRPT != nil {
645 uri := url.URL{
646 Scheme: "mailto",
647 Opaque: smtp.NewAddress(domConf.TLSRPT.ParsedLocalpart, domConf.TLSRPT.DNSDomain).Pack(false),
648 }
649 tlsrptr := tlsrpt.Record{Version: "TLSRPTv1", RUAs: [][]tlsrpt.RUA{{tlsrpt.RUA(uri.String())}}}
650 records = append(records,
651 "; Request reporting about TLS failures.",
652 fmt.Sprintf(`_smtp._tls.%s. TXT "%s"`, d, tlsrptr.String()),
653 "",
654 )
655 }
656
657 if domConf.ClientSettingsDomain != "" && domConf.ClientSettingsDNSDomain != Conf.Static.HostnameDomain {
658 records = append(records,
659 "; Client settings will reference a subdomain of the hosted domain, making it",
660 "; easier to migrate to a different server in the future by not requiring settings",
661 "; in all clients to be updated.",
662 fmt.Sprintf(`%-*s CNAME %s.`, 20+len(d), domConf.ClientSettingsDNSDomain.ASCII+".", h),
663 "",
664 )
665 }
666
667 records = append(records,
668 "; Autoconfig is used by Thunderbird. Autodiscover is (in theory) used by Microsoft.",
669 fmt.Sprintf(`autoconfig.%s. CNAME %s.`, d, h),
670 fmt.Sprintf(`_autodiscover._tcp.%s. SRV 0 1 443 %s.`, d, h),
671 "",
672
673 // ../rfc/6186:133 ../rfc/8314:692
674 "; For secure IMAP and submission autoconfig, point to mail host.",
675 fmt.Sprintf(`_imaps._tcp.%s. SRV 0 1 993 %s.`, d, h),
676 fmt.Sprintf(`_submissions._tcp.%s. SRV 0 1 465 %s.`, d, h),
677 "",
678 // ../rfc/6186:242
679 "; Next records specify POP3 and non-TLS ports are not to be used.",
680 "; These are optional and safe to leave out (e.g. if you have to click a lot in a",
681 "; DNS admin web interface).",
682 fmt.Sprintf(`_imap._tcp.%s. SRV 0 1 143 .`, d),
683 fmt.Sprintf(`_submission._tcp.%s. SRV 0 1 587 .`, d),
684 fmt.Sprintf(`_pop3._tcp.%s. SRV 0 1 110 .`, d),
685 fmt.Sprintf(`_pop3s._tcp.%s. SRV 0 1 995 .`, d),
686 )
687
688 if certIssuerDomainName != "" {
689 // ../rfc/8659:18 for CAA records.
690 records = append(records,
691 "",
692 "; Optional:",
693 "; You could mark Let's Encrypt as the only Certificate Authority allowed to",
694 "; sign TLS certificates for your domain.",
695 fmt.Sprintf(`%s. CAA 0 issue "%s"`, d, certIssuerDomainName),
696 )
697 if acmeAccountURI != "" {
698 // ../rfc/8657:99 for accounturi.
699 // ../rfc/8657:147 for validationmethods.
700 records = append(records,
701 ";",
702 "; Optionally limit certificates for this domain to the account ID and methods used by mox.",
703 fmt.Sprintf(`;; %s. CAA 0 issue "%s; accounturi=%s; validationmethods=tls-alpn-01,http-01"`, d, certIssuerDomainName, acmeAccountURI),
704 ";",
705 "; Or alternatively only limit for email-specific subdomains, so you can use",
706 "; other accounts/methods for other subdomains.",
707 fmt.Sprintf(`;; autoconfig.%s. CAA 0 issue "%s; accounturi=%s; validationmethods=tls-alpn-01,http-01"`, d, certIssuerDomainName, acmeAccountURI),
708 fmt.Sprintf(`;; mta-sts.%s. CAA 0 issue "%s; accounturi=%s; validationmethods=tls-alpn-01,http-01"`, d, certIssuerDomainName, acmeAccountURI),
709 )
710 if domConf.ClientSettingsDomain != "" && domConf.ClientSettingsDNSDomain != Conf.Static.HostnameDomain {
711 records = append(records,
712 fmt.Sprintf(`;; %-*s CAA 0 issue "%s; accounturi=%s; validationmethods=tls-alpn-01,http-01"`, 20-3+len(d), domConf.ClientSettingsDNSDomain.ASCII, certIssuerDomainName, acmeAccountURI),
713 )
714 }
715 if strings.HasSuffix(h, "."+d) {
716 records = append(records,
717 ";",
718 "; And the mail hostname.",
719 fmt.Sprintf(`;; %-*s CAA 0 issue "%s; accounturi=%s; validationmethods=tls-alpn-01,http-01"`, 20-3+len(d), h+".", certIssuerDomainName, acmeAccountURI),
720 )
721 }
722 } else {
723 // The string "will be suggested" is used by
724 // ../testdata/integration/moxacmepebble.sh and ../testdata/integration/moxmail2.sh
725 // as end of DNS records.
726 records = append(records,
727 ";",
728 "; Note: After starting up, once an ACME account has been created, CAA records",
729 "; that restrict issuance to the account will be suggested.",
730 )
731 }
732 }
733 return records, nil
734}
735
736// AccountAdd adds an account and an initial address and reloads the configuration.
737//
738// The new account does not have a password, so cannot yet log in. Email can be
739// delivered.
740//
741// Catchall addresses are not supported for AccountAdd. Add separately with AddressAdd.
742func AccountAdd(ctx context.Context, account, address string) (rerr error) {
743 log := pkglog.WithContext(ctx)
744 defer func() {
745 if rerr != nil {
746 log.Errorx("adding account", rerr, slog.String("account", account), slog.String("address", address))
747 }
748 }()
749
750 addr, err := smtp.ParseAddress(address)
751 if err != nil {
752 return fmt.Errorf("parsing email address: %v", err)
753 }
754
755 Conf.dynamicMutex.Lock()
756 defer Conf.dynamicMutex.Unlock()
757
758 c := Conf.Dynamic
759 if _, ok := c.Accounts[account]; ok {
760 return fmt.Errorf("account already present")
761 }
762
763 if err := checkAddressAvailable(addr); err != nil {
764 return fmt.Errorf("address not available: %v", err)
765 }
766
767 // Compose new config without modifying existing data structures. If we fail, we
768 // leave no trace.
769 nc := c
770 nc.Accounts = map[string]config.Account{}
771 for name, a := range c.Accounts {
772 nc.Accounts[name] = a
773 }
774 nc.Accounts[account] = MakeAccountConfig(addr)
775
776 if err := writeDynamic(ctx, log, nc); err != nil {
777 return fmt.Errorf("writing domains.conf: %v", err)
778 }
779 log.Info("account added", slog.String("account", account), slog.Any("address", addr))
780 return nil
781}
782
783// AccountRemove removes an account and reloads the configuration.
784func AccountRemove(ctx context.Context, account string) (rerr error) {
785 log := pkglog.WithContext(ctx)
786 defer func() {
787 if rerr != nil {
788 log.Errorx("adding account", rerr, slog.String("account", account))
789 }
790 }()
791
792 Conf.dynamicMutex.Lock()
793 defer Conf.dynamicMutex.Unlock()
794
795 c := Conf.Dynamic
796 if _, ok := c.Accounts[account]; !ok {
797 return fmt.Errorf("account does not exist")
798 }
799
800 // Compose new config without modifying existing data structures. If we fail, we
801 // leave no trace.
802 nc := c
803 nc.Accounts = map[string]config.Account{}
804 for name, a := range c.Accounts {
805 if name != account {
806 nc.Accounts[name] = a
807 }
808 }
809
810 if err := writeDynamic(ctx, log, nc); err != nil {
811 return fmt.Errorf("writing domains.conf: %v", err)
812 }
813 log.Info("account removed", slog.String("account", account))
814 return nil
815}
816
817// checkAddressAvailable checks that the address after canonicalization is not
818// already configured, and that its localpart does not contain the catchall
819// localpart separator.
820//
821// Must be called with config lock held.
822func checkAddressAvailable(addr smtp.Address) error {
823 if dc, ok := Conf.Dynamic.Domains[addr.Domain.Name()]; !ok {
824 return fmt.Errorf("domain does not exist")
825 } else if lp, err := CanonicalLocalpart(addr.Localpart, dc); err != nil {
826 return fmt.Errorf("canonicalizing localpart: %v", err)
827 } else if _, ok := Conf.accountDestinations[smtp.NewAddress(lp, addr.Domain).String()]; ok {
828 return fmt.Errorf("canonicalized address %s already configured", smtp.NewAddress(lp, addr.Domain))
829 } else if dc.LocalpartCatchallSeparator != "" && strings.Contains(string(addr.Localpart), dc.LocalpartCatchallSeparator) {
830 return fmt.Errorf("localpart cannot include domain catchall separator %s", dc.LocalpartCatchallSeparator)
831 }
832 return nil
833}
834
835// AddressAdd adds an email address to an account and reloads the configuration. If
836// address starts with an @ it is treated as a catchall address for the domain.
837func AddressAdd(ctx context.Context, address, account string) (rerr error) {
838 log := pkglog.WithContext(ctx)
839 defer func() {
840 if rerr != nil {
841 log.Errorx("adding address", rerr, slog.String("address", address), slog.String("account", account))
842 }
843 }()
844
845 Conf.dynamicMutex.Lock()
846 defer Conf.dynamicMutex.Unlock()
847
848 c := Conf.Dynamic
849 a, ok := c.Accounts[account]
850 if !ok {
851 return fmt.Errorf("account does not exist")
852 }
853
854 var destAddr string
855 if strings.HasPrefix(address, "@") {
856 d, err := dns.ParseDomain(address[1:])
857 if err != nil {
858 return fmt.Errorf("parsing domain: %v", err)
859 }
860 dname := d.Name()
861 destAddr = "@" + dname
862 if _, ok := Conf.Dynamic.Domains[dname]; !ok {
863 return fmt.Errorf("domain does not exist")
864 } else if _, ok := Conf.accountDestinations[destAddr]; ok {
865 return fmt.Errorf("catchall address already configured for domain")
866 }
867 } else {
868 addr, err := smtp.ParseAddress(address)
869 if err != nil {
870 return fmt.Errorf("parsing email address: %v", err)
871 }
872
873 if err := checkAddressAvailable(addr); err != nil {
874 return fmt.Errorf("address not available: %v", err)
875 }
876 destAddr = addr.String()
877 }
878
879 // Compose new config without modifying existing data structures. If we fail, we
880 // leave no trace.
881 nc := c
882 nc.Accounts = map[string]config.Account{}
883 for name, a := range c.Accounts {
884 nc.Accounts[name] = a
885 }
886 nd := map[string]config.Destination{}
887 for name, d := range a.Destinations {
888 nd[name] = d
889 }
890 nd[destAddr] = config.Destination{}
891 a.Destinations = nd
892 nc.Accounts[account] = a
893
894 if err := writeDynamic(ctx, log, nc); err != nil {
895 return fmt.Errorf("writing domains.conf: %v", err)
896 }
897 log.Info("address added", slog.String("address", address), slog.String("account", account))
898 return nil
899}
900
901// AddressRemove removes an email address and reloads the configuration.
902func AddressRemove(ctx context.Context, address string) (rerr error) {
903 log := pkglog.WithContext(ctx)
904 defer func() {
905 if rerr != nil {
906 log.Errorx("removing address", rerr, slog.String("address", address))
907 }
908 }()
909
910 Conf.dynamicMutex.Lock()
911 defer Conf.dynamicMutex.Unlock()
912
913 ad, ok := Conf.accountDestinations[address]
914 if !ok {
915 return fmt.Errorf("address does not exists")
916 }
917
918 // Compose new config without modifying existing data structures. If we fail, we
919 // leave no trace.
920 a, ok := Conf.Dynamic.Accounts[ad.Account]
921 if !ok {
922 return fmt.Errorf("internal error: cannot find account")
923 }
924 na := a
925 na.Destinations = map[string]config.Destination{}
926 var dropped bool
927 for destAddr, d := range a.Destinations {
928 if destAddr != address {
929 na.Destinations[destAddr] = d
930 } else {
931 dropped = true
932 }
933 }
934 if !dropped {
935 return fmt.Errorf("address not removed, likely a postmaster/reporting address")
936 }
937 nc := Conf.Dynamic
938 nc.Accounts = map[string]config.Account{}
939 for name, a := range Conf.Dynamic.Accounts {
940 nc.Accounts[name] = a
941 }
942 nc.Accounts[ad.Account] = na
943
944 if err := writeDynamic(ctx, log, nc); err != nil {
945 return fmt.Errorf("writing domains.conf: %v", err)
946 }
947 log.Info("address removed", slog.String("address", address), slog.String("account", ad.Account))
948 return nil
949}
950
951// AccountFullNameSave updates the full name for an account and reloads the configuration.
952func AccountFullNameSave(ctx context.Context, account, fullName string) (rerr error) {
953 log := pkglog.WithContext(ctx)
954 defer func() {
955 if rerr != nil {
956 log.Errorx("saving account full name", rerr, slog.String("account", account))
957 }
958 }()
959
960 Conf.dynamicMutex.Lock()
961 defer Conf.dynamicMutex.Unlock()
962
963 c := Conf.Dynamic
964 acc, ok := c.Accounts[account]
965 if !ok {
966 return fmt.Errorf("account not present")
967 }
968
969 // Compose new config without modifying existing data structures. If we fail, we
970 // leave no trace.
971 nc := c
972 nc.Accounts = map[string]config.Account{}
973 for name, a := range c.Accounts {
974 nc.Accounts[name] = a
975 }
976
977 acc.FullName = fullName
978 nc.Accounts[account] = acc
979
980 if err := writeDynamic(ctx, log, nc); err != nil {
981 return fmt.Errorf("writing domains.conf: %v", err)
982 }
983 log.Info("account full name saved", slog.String("account", account))
984 return nil
985}
986
987// DestinationSave updates a destination for an account and reloads the configuration.
988func DestinationSave(ctx context.Context, account, destName string, newDest config.Destination) (rerr error) {
989 log := pkglog.WithContext(ctx)
990 defer func() {
991 if rerr != nil {
992 log.Errorx("saving destination", rerr,
993 slog.String("account", account),
994 slog.String("destname", destName),
995 slog.Any("destination", newDest))
996 }
997 }()
998
999 Conf.dynamicMutex.Lock()
1000 defer Conf.dynamicMutex.Unlock()
1001
1002 c := Conf.Dynamic
1003 acc, ok := c.Accounts[account]
1004 if !ok {
1005 return fmt.Errorf("account not present")
1006 }
1007
1008 if _, ok := acc.Destinations[destName]; !ok {
1009 return fmt.Errorf("destination not present")
1010 }
1011
1012 // Compose new config without modifying existing data structures. If we fail, we
1013 // leave no trace.
1014 nc := c
1015 nc.Accounts = map[string]config.Account{}
1016 for name, a := range c.Accounts {
1017 nc.Accounts[name] = a
1018 }
1019 nd := map[string]config.Destination{}
1020 for dn, d := range acc.Destinations {
1021 nd[dn] = d
1022 }
1023 nd[destName] = newDest
1024 nacc := nc.Accounts[account]
1025 nacc.Destinations = nd
1026 nc.Accounts[account] = nacc
1027
1028 if err := writeDynamic(ctx, log, nc); err != nil {
1029 return fmt.Errorf("writing domains.conf: %v", err)
1030 }
1031 log.Info("destination saved", slog.String("account", account), slog.String("destname", destName))
1032 return nil
1033}
1034
1035// AccountLimitsSave saves new message sending limits for an account.
1036func AccountLimitsSave(ctx context.Context, account string, maxOutgoingMessagesPerDay, maxFirstTimeRecipientsPerDay int, quotaMessageSize int64) (rerr error) {
1037 log := pkglog.WithContext(ctx)
1038 defer func() {
1039 if rerr != nil {
1040 log.Errorx("saving account limits", rerr, slog.String("account", account))
1041 }
1042 }()
1043
1044 Conf.dynamicMutex.Lock()
1045 defer Conf.dynamicMutex.Unlock()
1046
1047 c := Conf.Dynamic
1048 acc, ok := c.Accounts[account]
1049 if !ok {
1050 return fmt.Errorf("account not present")
1051 }
1052
1053 // Compose new config without modifying existing data structures. If we fail, we
1054 // leave no trace.
1055 nc := c
1056 nc.Accounts = map[string]config.Account{}
1057 for name, a := range c.Accounts {
1058 nc.Accounts[name] = a
1059 }
1060 acc.MaxOutgoingMessagesPerDay = maxOutgoingMessagesPerDay
1061 acc.MaxFirstTimeRecipientsPerDay = maxFirstTimeRecipientsPerDay
1062 acc.QuotaMessageSize = quotaMessageSize
1063 nc.Accounts[account] = acc
1064
1065 if err := writeDynamic(ctx, log, nc); err != nil {
1066 return fmt.Errorf("writing domains.conf: %v", err)
1067 }
1068 log.Info("account limits saved", slog.String("account", account))
1069 return nil
1070}
1071
1072func MonitorDNSBLsSave(ctx context.Context, zones []dns.Domain) (rerr error) {
1073 log := pkglog.WithContext(ctx)
1074 defer func() {
1075 if rerr != nil {
1076 log.Errorx("saving monitor dnsbl zones", rerr)
1077 }
1078 }()
1079
1080 Conf.dynamicMutex.Lock()
1081 defer Conf.dynamicMutex.Unlock()
1082
1083 c := Conf.Dynamic
1084
1085 // Compose new config without modifying existing data structures. If we fail, we
1086 // leave no trace.
1087 nc := c
1088 nc.MonitorDNSBLs = make([]string, len(zones))
1089 nc.MonitorDNSBLZones = nil
1090 for i, z := range zones {
1091 nc.MonitorDNSBLs[i] = z.Name()
1092 }
1093
1094 if err := writeDynamic(ctx, log, nc); err != nil {
1095 return fmt.Errorf("writing domains.conf: %v", err)
1096 }
1097 log.Info("monitor dnsbl zones saved")
1098 return nil
1099}
1100
1101type TLSMode uint8
1102
1103const (
1104 TLSModeImmediate TLSMode = 0
1105 TLSModeSTARTTLS TLSMode = 1
1106 TLSModeNone TLSMode = 2
1107)
1108
1109type ProtocolConfig struct {
1110 Host dns.Domain
1111 Port int
1112 TLSMode TLSMode
1113}
1114
1115type ClientConfig struct {
1116 IMAP ProtocolConfig
1117 Submission ProtocolConfig
1118}
1119
1120// ClientConfigDomain returns a single IMAP and Submission client configuration for
1121// a domain.
1122func ClientConfigDomain(d dns.Domain) (rconfig ClientConfig, rerr error) {
1123 var haveIMAP, haveSubmission bool
1124
1125 domConf, ok := Conf.Domain(d)
1126 if !ok {
1127 return ClientConfig{}, fmt.Errorf("unknown domain")
1128 }
1129
1130 gather := func(l config.Listener) (done bool) {
1131 host := Conf.Static.HostnameDomain
1132 if l.Hostname != "" {
1133 host = l.HostnameDomain
1134 }
1135 if domConf.ClientSettingsDomain != "" {
1136 host = domConf.ClientSettingsDNSDomain
1137 }
1138 if !haveIMAP && l.IMAPS.Enabled {
1139 rconfig.IMAP.Host = host
1140 rconfig.IMAP.Port = config.Port(l.IMAPS.Port, 993)
1141 rconfig.IMAP.TLSMode = TLSModeImmediate
1142 haveIMAP = true
1143 }
1144 if !haveIMAP && l.IMAP.Enabled {
1145 rconfig.IMAP.Host = host
1146 rconfig.IMAP.Port = config.Port(l.IMAP.Port, 143)
1147 rconfig.IMAP.TLSMode = TLSModeSTARTTLS
1148 if l.TLS == nil {
1149 rconfig.IMAP.TLSMode = TLSModeNone
1150 }
1151 haveIMAP = true
1152 }
1153 if !haveSubmission && l.Submissions.Enabled {
1154 rconfig.Submission.Host = host
1155 rconfig.Submission.Port = config.Port(l.Submissions.Port, 465)
1156 rconfig.Submission.TLSMode = TLSModeImmediate
1157 haveSubmission = true
1158 }
1159 if !haveSubmission && l.Submission.Enabled {
1160 rconfig.Submission.Host = host
1161 rconfig.Submission.Port = config.Port(l.Submission.Port, 587)
1162 rconfig.Submission.TLSMode = TLSModeSTARTTLS
1163 if l.TLS == nil {
1164 rconfig.Submission.TLSMode = TLSModeNone
1165 }
1166 haveSubmission = true
1167 }
1168 return haveIMAP && haveSubmission
1169 }
1170
1171 // Look at the public listener first. Most likely the intended configuration.
1172 if public, ok := Conf.Static.Listeners["public"]; ok {
1173 if gather(public) {
1174 return
1175 }
1176 }
1177 // Go through the other listeners in consistent order.
1178 names := maps.Keys(Conf.Static.Listeners)
1179 sort.Strings(names)
1180 for _, name := range names {
1181 if gather(Conf.Static.Listeners[name]) {
1182 return
1183 }
1184 }
1185 return ClientConfig{}, fmt.Errorf("no listeners found for imap and/or submission")
1186}
1187
1188// ClientConfigs holds the client configuration for IMAP/Submission for a
1189// domain.
1190type ClientConfigs struct {
1191 Entries []ClientConfigsEntry
1192}
1193
1194type ClientConfigsEntry struct {
1195 Protocol string
1196 Host dns.Domain
1197 Port int
1198 Listener string
1199 Note string
1200}
1201
1202// ClientConfigsDomain returns the client configs for IMAP/Submission for a
1203// domain.
1204func ClientConfigsDomain(d dns.Domain) (ClientConfigs, error) {
1205 domConf, ok := Conf.Domain(d)
1206 if !ok {
1207 return ClientConfigs{}, fmt.Errorf("unknown domain")
1208 }
1209
1210 c := ClientConfigs{}
1211 c.Entries = []ClientConfigsEntry{}
1212 var listeners []string
1213
1214 for name := range Conf.Static.Listeners {
1215 listeners = append(listeners, name)
1216 }
1217 sort.Slice(listeners, func(i, j int) bool {
1218 return listeners[i] < listeners[j]
1219 })
1220
1221 note := func(tls bool, requiretls bool) string {
1222 if !tls {
1223 return "plain text, no STARTTLS configured"
1224 }
1225 if requiretls {
1226 return "STARTTLS required"
1227 }
1228 return "STARTTLS optional"
1229 }
1230
1231 for _, name := range listeners {
1232 l := Conf.Static.Listeners[name]
1233 host := Conf.Static.HostnameDomain
1234 if l.Hostname != "" {
1235 host = l.HostnameDomain
1236 }
1237 if domConf.ClientSettingsDomain != "" {
1238 host = domConf.ClientSettingsDNSDomain
1239 }
1240 if l.Submissions.Enabled {
1241 c.Entries = append(c.Entries, ClientConfigsEntry{"Submission (SMTP)", host, config.Port(l.Submissions.Port, 465), name, "with TLS"})
1242 }
1243 if l.IMAPS.Enabled {
1244 c.Entries = append(c.Entries, ClientConfigsEntry{"IMAP", host, config.Port(l.IMAPS.Port, 993), name, "with TLS"})
1245 }
1246 if l.Submission.Enabled {
1247 c.Entries = append(c.Entries, ClientConfigsEntry{"Submission (SMTP)", host, config.Port(l.Submission.Port, 587), name, note(l.TLS != nil, !l.Submission.NoRequireSTARTTLS)})
1248 }
1249 if l.IMAP.Enabled {
1250 c.Entries = append(c.Entries, ClientConfigsEntry{"IMAP", host, config.Port(l.IMAPS.Port, 143), name, note(l.TLS != nil, !l.IMAP.NoRequireSTARTTLS)})
1251 }
1252 }
1253
1254 return c, nil
1255}
1256
1257// IPs returns ip addresses we may be listening/receiving mail on or
1258// connecting/sending from to the outside.
1259func IPs(ctx context.Context, receiveOnly bool) ([]net.IP, error) {
1260 log := pkglog.WithContext(ctx)
1261
1262 // Try to gather all IPs we are listening on by going through the config.
1263 // If we encounter 0.0.0.0 or ::, we'll gather all local IPs afterwards.
1264 var ips []net.IP
1265 var ipv4all, ipv6all bool
1266 for _, l := range Conf.Static.Listeners {
1267 // If NATed, we don't know our external IPs.
1268 if l.IPsNATed {
1269 return nil, nil
1270 }
1271 check := l.IPs
1272 if len(l.NATIPs) > 0 {
1273 check = l.NATIPs
1274 }
1275 for _, s := range check {
1276 ip := net.ParseIP(s)
1277 if ip.IsUnspecified() {
1278 if ip.To4() != nil {
1279 ipv4all = true
1280 } else {
1281 ipv6all = true
1282 }
1283 continue
1284 }
1285 ips = append(ips, ip)
1286 }
1287 }
1288
1289 // We'll list the IPs on the interfaces. How useful is this? There is a good chance
1290 // we're listening on all addresses because of a load balancer/firewall.
1291 if ipv4all || ipv6all {
1292 ifaces, err := net.Interfaces()
1293 if err != nil {
1294 return nil, fmt.Errorf("listing network interfaces: %v", err)
1295 }
1296 for _, iface := range ifaces {
1297 if iface.Flags&net.FlagUp == 0 {
1298 continue
1299 }
1300 addrs, err := iface.Addrs()
1301 if err != nil {
1302 return nil, fmt.Errorf("listing addresses for network interface: %v", err)
1303 }
1304 if len(addrs) == 0 {
1305 continue
1306 }
1307
1308 for _, addr := range addrs {
1309 ip, _, err := net.ParseCIDR(addr.String())
1310 if err != nil {
1311 log.Errorx("bad interface addr", err, slog.Any("address", addr))
1312 continue
1313 }
1314 v4 := ip.To4() != nil
1315 if ipv4all && v4 || ipv6all && !v4 {
1316 ips = append(ips, ip)
1317 }
1318 }
1319 }
1320 }
1321
1322 if receiveOnly {
1323 return ips, nil
1324 }
1325
1326 for _, t := range Conf.Static.Transports {
1327 if t.Socks != nil {
1328 ips = append(ips, t.Socks.IPs...)
1329 }
1330 }
1331
1332 return ips, nil
1333}
1334