This commit is contained in:
parent
d8867dfc4a
commit
657f4922b4
@ -23,6 +23,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"git.pcux.cn/xuans/Smail/config"
|
"git.pcux.cn/xuans/Smail/config"
|
||||||
@ -30,9 +31,11 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"net/smtp"
|
"net/smtp"
|
||||||
"net/textproto"
|
"net/textproto"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@ -47,8 +50,11 @@ const (
|
|||||||
outboundSMTPTLSHandshakeTimeout = 2 * time.Second
|
outboundSMTPTLSHandshakeTimeout = 2 * time.Second
|
||||||
outboundSMTPTransactionTimeout = 2 * time.Minute
|
outboundSMTPTransactionTimeout = 2 * time.Minute
|
||||||
outboundSMTPQuitWriteTimeout = 250 * time.Millisecond
|
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.
|
// A Client represents a client connection to an SMTP server.
|
||||||
type Client struct {
|
type Client struct {
|
||||||
// Text is the textproto.Conn used by the Client. It is exported to allow for
|
// 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 {
|
if len(addresses) == 0 {
|
||||||
return nil, errors.New("smtp: remote address is unavailable")
|
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
|
return addresses, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"math/big"
|
"math/big"
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
"net/textproto"
|
"net/textproto"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"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) {
|
func TestSendMailUnsafeRejectsNonLoopbackTargets(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user