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 "net"
15 "net/url"
16 "os"
17 "path/filepath"
18 "sort"
19 "strings"
20 "time"
21
22 "golang.org/x/exp/maps"
23 "golang.org/x/exp/slog"
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
1072type TLSMode uint8
1073
1074const (
1075 TLSModeImmediate TLSMode = 0
1076 TLSModeSTARTTLS TLSMode = 1
1077 TLSModeNone TLSMode = 2
1078)
1079
1080type ProtocolConfig struct {
1081 Host dns.Domain
1082 Port int
1083 TLSMode TLSMode
1084}
1085
1086type ClientConfig struct {
1087 IMAP ProtocolConfig
1088 Submission ProtocolConfig
1089}
1090
1091// ClientConfigDomain returns a single IMAP and Submission client configuration for
1092// a domain.
1093func ClientConfigDomain(d dns.Domain) (rconfig ClientConfig, rerr error) {
1094 var haveIMAP, haveSubmission bool
1095
1096 domConf, ok := Conf.Domain(d)
1097 if !ok {
1098 return ClientConfig{}, fmt.Errorf("unknown domain")
1099 }
1100
1101 gather := func(l config.Listener) (done bool) {
1102 host := Conf.Static.HostnameDomain
1103 if l.Hostname != "" {
1104 host = l.HostnameDomain
1105 }
1106 if domConf.ClientSettingsDomain != "" {
1107 host = domConf.ClientSettingsDNSDomain
1108 }
1109 if !haveIMAP && l.IMAPS.Enabled {
1110 rconfig.IMAP.Host = host
1111 rconfig.IMAP.Port = config.Port(l.IMAPS.Port, 993)
1112 rconfig.IMAP.TLSMode = TLSModeImmediate
1113 haveIMAP = true
1114 }
1115 if !haveIMAP && l.IMAP.Enabled {
1116 rconfig.IMAP.Host = host
1117 rconfig.IMAP.Port = config.Port(l.IMAP.Port, 143)
1118 rconfig.IMAP.TLSMode = TLSModeSTARTTLS
1119 if l.TLS == nil {
1120 rconfig.IMAP.TLSMode = TLSModeNone
1121 }
1122 haveIMAP = true
1123 }
1124 if !haveSubmission && l.Submissions.Enabled {
1125 rconfig.Submission.Host = host
1126 rconfig.Submission.Port = config.Port(l.Submissions.Port, 465)
1127 rconfig.Submission.TLSMode = TLSModeImmediate
1128 haveSubmission = true
1129 }
1130 if !haveSubmission && l.Submission.Enabled {
1131 rconfig.Submission.Host = host
1132 rconfig.Submission.Port = config.Port(l.Submission.Port, 587)
1133 rconfig.Submission.TLSMode = TLSModeSTARTTLS
1134 if l.TLS == nil {
1135 rconfig.Submission.TLSMode = TLSModeNone
1136 }
1137 haveSubmission = true
1138 }
1139 return haveIMAP && haveSubmission
1140 }
1141
1142 // Look at the public listener first. Most likely the intended configuration.
1143 if public, ok := Conf.Static.Listeners["public"]; ok {
1144 if gather(public) {
1145 return
1146 }
1147 }
1148 // Go through the other listeners in consistent order.
1149 names := maps.Keys(Conf.Static.Listeners)
1150 sort.Strings(names)
1151 for _, name := range names {
1152 if gather(Conf.Static.Listeners[name]) {
1153 return
1154 }
1155 }
1156 return ClientConfig{}, fmt.Errorf("no listeners found for imap and/or submission")
1157}
1158
1159// ClientConfigs holds the client configuration for IMAP/Submission for a
1160// domain.
1161type ClientConfigs struct {
1162 Entries []ClientConfigsEntry
1163}
1164
1165type ClientConfigsEntry struct {
1166 Protocol string
1167 Host dns.Domain
1168 Port int
1169 Listener string
1170 Note string
1171}
1172
1173// ClientConfigsDomain returns the client configs for IMAP/Submission for a
1174// domain.
1175func ClientConfigsDomain(d dns.Domain) (ClientConfigs, error) {
1176 domConf, ok := Conf.Domain(d)
1177 if !ok {
1178 return ClientConfigs{}, fmt.Errorf("unknown domain")
1179 }
1180
1181 c := ClientConfigs{}
1182 c.Entries = []ClientConfigsEntry{}
1183 var listeners []string
1184
1185 for name := range Conf.Static.Listeners {
1186 listeners = append(listeners, name)
1187 }
1188 sort.Slice(listeners, func(i, j int) bool {
1189 return listeners[i] < listeners[j]
1190 })
1191
1192 note := func(tls bool, requiretls bool) string {
1193 if !tls {
1194 return "plain text, no STARTTLS configured"
1195 }
1196 if requiretls {
1197 return "STARTTLS required"
1198 }
1199 return "STARTTLS optional"
1200 }
1201
1202 for _, name := range listeners {
1203 l := Conf.Static.Listeners[name]
1204 host := Conf.Static.HostnameDomain
1205 if l.Hostname != "" {
1206 host = l.HostnameDomain
1207 }
1208 if domConf.ClientSettingsDomain != "" {
1209 host = domConf.ClientSettingsDNSDomain
1210 }
1211 if l.Submissions.Enabled {
1212 c.Entries = append(c.Entries, ClientConfigsEntry{"Submission (SMTP)", host, config.Port(l.Submissions.Port, 465), name, "with TLS"})
1213 }
1214 if l.IMAPS.Enabled {
1215 c.Entries = append(c.Entries, ClientConfigsEntry{"IMAP", host, config.Port(l.IMAPS.Port, 993), name, "with TLS"})
1216 }
1217 if l.Submission.Enabled {
1218 c.Entries = append(c.Entries, ClientConfigsEntry{"Submission (SMTP)", host, config.Port(l.Submission.Port, 587), name, note(l.TLS != nil, !l.Submission.NoRequireSTARTTLS)})
1219 }
1220 if l.IMAP.Enabled {
1221 c.Entries = append(c.Entries, ClientConfigsEntry{"IMAP", host, config.Port(l.IMAPS.Port, 143), name, note(l.TLS != nil, !l.IMAP.NoRequireSTARTTLS)})
1222 }
1223 }
1224
1225 return c, nil
1226}
1227
1228// IPs returns ip addresses we may be listening/receiving mail on or
1229// connecting/sending from to the outside.
1230func IPs(ctx context.Context, receiveOnly bool) ([]net.IP, error) {
1231 log := pkglog.WithContext(ctx)
1232
1233 // Try to gather all IPs we are listening on by going through the config.
1234 // If we encounter 0.0.0.0 or ::, we'll gather all local IPs afterwards.
1235 var ips []net.IP
1236 var ipv4all, ipv6all bool
1237 for _, l := range Conf.Static.Listeners {
1238 // If NATed, we don't know our external IPs.
1239 if l.IPsNATed {
1240 return nil, nil
1241 }
1242 check := l.IPs
1243 if len(l.NATIPs) > 0 {
1244 check = l.NATIPs
1245 }
1246 for _, s := range check {
1247 ip := net.ParseIP(s)
1248 if ip.IsUnspecified() {
1249 if ip.To4() != nil {
1250 ipv4all = true
1251 } else {
1252 ipv6all = true
1253 }
1254 continue
1255 }
1256 ips = append(ips, ip)
1257 }
1258 }
1259
1260 // We'll list the IPs on the interfaces. How useful is this? There is a good chance
1261 // we're listening on all addresses because of a load balancer/firewall.
1262 if ipv4all || ipv6all {
1263 ifaces, err := net.Interfaces()
1264 if err != nil {
1265 return nil, fmt.Errorf("listing network interfaces: %v", err)
1266 }
1267 for _, iface := range ifaces {
1268 if iface.Flags&net.FlagUp == 0 {
1269 continue
1270 }
1271 addrs, err := iface.Addrs()
1272 if err != nil {
1273 return nil, fmt.Errorf("listing addresses for network interface: %v", err)
1274 }
1275 if len(addrs) == 0 {
1276 continue
1277 }
1278
1279 for _, addr := range addrs {
1280 ip, _, err := net.ParseCIDR(addr.String())
1281 if err != nil {
1282 log.Errorx("bad interface addr", err, slog.Any("address", addr))
1283 continue
1284 }
1285 v4 := ip.To4() != nil
1286 if ipv4all && v4 || ipv6all && !v4 {
1287 ips = append(ips, ip)
1288 }
1289 }
1290 }
1291 }
1292
1293 if receiveOnly {
1294 return ips, nil
1295 }
1296
1297 for _, t := range Conf.Static.Transports {
1298 if t.Socks != nil {
1299 ips = append(ips, t.Socks.IPs...)
1300 }
1301 }
1302
1303 return ips, nil
1304}
1305