修复代理环境邮件目标解析
Some checks are pending
Security Checks / security (push) Waiting to run

This commit is contained in:
smail 2026-09-16 14:27:53 +08:00
parent d8867dfc4a
commit 657f4922b4
2 changed files with 100 additions and 0 deletions

View File

@ -23,6 +23,7 @@ import (
"context"
"crypto/tls"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"git.pcux.cn/xuans/Smail/config"
@ -30,9 +31,11 @@ import (
log "github.com/sirupsen/logrus"
"io"
"net"
"net/http"
"net/netip"
"net/smtp"
"net/textproto"
"net/url"
"strings"
"time"
)
@ -47,8 +50,11 @@ const (
outboundSMTPTLSHandshakeTimeout = 2 * time.Second
outboundSMTPTransactionTimeout = 2 * time.Minute
outboundSMTPQuitWriteTimeout = 250 * time.Millisecond
remoteAddressResponseLimit = 64 * 1024
)
var benchmarkNetworkPrefix = netip.MustParsePrefix("198.18.0.0/15")
// A Client represents a client connection to an SMTP server.
type Client struct {
// Text is the textproto.Conn used by the Client. It is exported to allow for
@ -189,6 +195,78 @@ func resolveRemoteAddresses(ctx context.Context, host string) ([]netip.Addr, err
if len(addresses) == 0 {
return nil, errors.New("smtp: remote address is unavailable")
}
if containsOnlyBenchmarkAddresses(addresses) {
addresses, err = resolveRemoteAddressesOverHTTPS(ctx, host)
if err != nil {
return nil, fmt.Errorf("smtp: resolve real remote address: %w", err)
}
}
return addresses, nil
}
func containsOnlyBenchmarkAddresses(addresses []netip.Addr) bool {
if len(addresses) == 0 {
return false
}
for _, address := range addresses {
if !benchmarkNetworkPrefix.Contains(address.Unmap()) {
return false
}
}
return true
}
func resolveRemoteAddressesOverHTTPS(ctx context.Context, host string) ([]netip.Addr, error) {
endpoint := "https://cloudflare-dns.com/dns-query?name=" + url.QueryEscape(host) + "&type=A"
request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}
request.Header.Set("Accept", "application/dns-json")
client := &http.Client{
Timeout: outboundSMTPDialTimeout,
CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
return http.ErrUseLastResponse
},
}
response, err := client.Do(request)
if err != nil {
return nil, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected DNS response status: %d", response.StatusCode)
}
var payload struct {
Status int `json:"Status"`
Answer []struct {
Type int `json:"type"`
Data string `json:"data"`
} `json:"Answer"`
}
decoder := json.NewDecoder(io.LimitReader(response.Body, remoteAddressResponseLimit))
if err = decoder.Decode(&payload); err != nil {
return nil, err
}
if payload.Status != 0 {
return nil, fmt.Errorf("DNS response code: %d", payload.Status)
}
addresses := make([]netip.Addr, 0, len(payload.Answer))
for _, answer := range payload.Answer {
if answer.Type != 1 {
continue
}
address, parseErr := netip.ParseAddr(strings.TrimSpace(answer.Data))
if parseErr == nil {
addresses = append(addresses, address.Unmap())
}
}
if len(addresses) == 0 {
return nil, errors.New("remote address is unavailable")
}
return addresses, nil
}

View File

@ -11,6 +11,7 @@ import (
"io"
"math/big"
"net"
"net/netip"
"net/textproto"
"strings"
"testing"
@ -42,6 +43,27 @@ func TestDialRejectsNonPublicRemoteAddress(t *testing.T) {
}
}
func TestContainsOnlyBenchmarkAddresses(t *testing.T) {
tests := []struct {
name string
addresses []netip.Addr
want bool
}{
{name: "empty", want: false},
{name: "fake IP", addresses: []netip.Addr{netip.MustParseAddr("198.18.1.77")}, want: true},
{name: "multiple fake IPs", addresses: []netip.Addr{netip.MustParseAddr("198.18.1.77"), netip.MustParseAddr("198.19.1.82")}, want: true},
{name: "public IP", addresses: []netip.Addr{netip.MustParseAddr("47.251.242.178")}, want: false},
{name: "mixed", addresses: []netip.Addr{netip.MustParseAddr("198.18.1.77"), netip.MustParseAddr("127.0.0.1")}, want: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := containsOnlyBenchmarkAddresses(tt.addresses); got != tt.want {
t.Fatalf("containsOnlyBenchmarkAddresses() = %v, want %v", got, tt.want)
}
})
}
}
func TestSendMailUnsafeRejectsNonLoopbackTargets(t *testing.T) {
tests := []struct {
name string