This commit is contained in:
parent
d8867dfc4a
commit
657f4922b4
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user