Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 79 additions & 26 deletions cli/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,21 @@ const (
hostSelectTypeWeighed hostSelectType = "weighed"
)

// hostPair holds a resolved host and the original hostname it was resolved from.
// originalHost is "" when --resolve-host is not used (no pinning needed).
type hostPair struct {
resolved string // IP:port to dial
originalHost string // original hostname (for S3 signing + SNI)
}

func newClient(ctx *cli.Context) func() (cl *minio.Client, done func()) {
hosts := parseHosts(ctx.String("host"), ctx.Bool("resolve-host"))
switch len(hosts) {
pairs := parseHostPairs(ctx.String("host"), ctx.Bool("resolve-host"))

switch len(pairs) {
case 0:
fatalIf(probe.NewError(errors.New("no host defined")), "Unable to create MinIO client")
case 1:
cl, err := getClient(ctx, hosts[0])
cl, err := getClient(ctx, pairs[0].resolved, pairs[0].originalHost)
fatalIf(probe.NewError(err), "Unable to create MinIO client")

return func() (*minio.Client, func()) {
Expand All @@ -70,9 +78,9 @@ func newClient(ctx *cli.Context) func() (cl *minio.Client, done func()) {
// Do round-robin.
var current int
var mu sync.Mutex
clients := make([]*minio.Client, len(hosts))
for i := range hosts {
cl, err := getClient(ctx, hosts[i])
clients := make([]*minio.Client, len(pairs))
for i := range pairs {
cl, err := getClient(ctx, pairs[i].resolved, pairs[i].originalHost)
fatalIf(probe.NewError(err), "Unable to create MinIO client")
clients[i] = cl
}
Expand All @@ -87,20 +95,20 @@ func newClient(ctx *cli.Context) func() (cl *minio.Client, done func()) {
// Keep track of handed out clients.
// Select random between the clients that have the fewest handed out.
var mu sync.Mutex
clients := make([]*minio.Client, len(hosts))
for i := range hosts {
cl, err := getClient(ctx, hosts[i])
clients := make([]*minio.Client, len(pairs))
for i := range pairs {
cl, err := getClient(ctx, pairs[i].resolved, pairs[i].originalHost)
fatalIf(probe.NewError(err), "Unable to create MinIO client")
clients[i] = cl
}
running := make([]int, len(hosts))
lastFinished := make([]time.Time, len(hosts))
running := make([]int, len(pairs))
lastFinished := make([]time.Time, len(pairs))
{
// Start with a random host
now := time.Now()
off := rand.New(rand.NewSource(time.Now().UnixNano())).Intn(len(hosts))
off := rand.New(rand.NewSource(time.Now().UnixNano())).Intn(len(pairs))
for i := range lastFinished {
lastFinished[i] = now.Add(time.Duration(i + off%len(hosts)))
lastFinished[i] = now.Add(time.Duration(i + off%len(pairs)))
}
}
find := func() int {
Expand Down Expand Up @@ -159,13 +167,19 @@ func detectLocalIP(host string) string {
}

// getClient creates a client with the specified host and the options set in the context.
func getClient(ctx *cli.Context, host string) (*minio.Client, error) {
// host is the resolved IP:port to dial; originalHost is the logical hostname for S3 signing
// and SNI (empty when --resolve-host is not used).
func getClient(ctx *cli.Context, host, originalHost string) (*minio.Client, error) {
var creds *credentials.Credentials
localIP := clientListenIP
if localIP == "" {
localIP = detectLocalIP(host)
}
transport := clientTransportWithLocalIP(ctx, localIP)
endpoint := host
if originalHost != "" {
endpoint = originalHost
}
transport := clientTransportWithLocalIP(ctx, localIP, host, originalHost)
switch strings.ToUpper(ctx.String("signature")) {
case "S3V4":
// if Signature version '4' use NewV4 directly.
Expand All @@ -189,7 +203,7 @@ func getClient(ctx *cli.Context, host string) (*minio.Client, error) {
if ctx.Bool("tls") || ctx.Bool("ktls") {
proto = "https"
}
stsEndPoint := fmt.Sprintf("%s://%s", proto, host)
stsEndPoint := fmt.Sprintf("%s://%s", proto, endpoint)
creds, err = credentials.NewSTSWebIdentity(stsEndPoint, func() (*credentials.WebIdentityToken, error) {
stsToken := ctx.String("sts-web-token")
if stsTokenFile, hasFilePrefix := strings.CutPrefix(stsToken, "file:"); hasFilePrefix {
Expand All @@ -214,7 +228,7 @@ func getClient(ctx *cli.Context, host string) (*minio.Client, error) {
} else if ctx.String("lookup") == "path" {
lookup = minio.BucketLookupPath
}
cl, err := minio.New(host, &minio.Options{
cl, err := minio.New(endpoint, &minio.Options{
Creds: creds,
Secure: ctx.Bool("tls") || ctx.Bool("ktls"),
Region: ctx.String("region"),
Expand All @@ -236,19 +250,21 @@ func getClient(ctx *cli.Context, host string) (*minio.Client, error) {
}

func clientTransport(ctx *cli.Context) http.RoundTripper {
return clientTransportWithLocalIP(ctx, "")
return clientTransportWithLocalIP(ctx, "", "", "")
}

// clientTransportWithLocalIP creates a transport that binds outbound connections
// to localIP (empty string means no binding, OS picks the source address).
func clientTransportWithLocalIP(ctx *cli.Context, localIP string) http.RoundTripper {
// When resolvedHost and originalHost are both non-empty, the transport also rewrites
// dial addresses from originalHost to resolvedHost and sets TLS SNI from originalHost.
func clientTransportWithLocalIP(ctx *cli.Context, localIP, resolvedHost, originalHost string) http.RoundTripper {
switch {
case ctx.Bool("ktls"):
return clientTransportKTLS(ctx, localIP)
return clientTransportKTLS(ctx, localIP, resolvedHost, originalHost)
case ctx.Bool("tls"):
return clientTransportTLS(ctx, localIP)
return clientTransportTLS(ctx, localIP, resolvedHost, originalHost)
default:
return clientTransportDefault(ctx, localIP)
return clientTransportDefault(ctx, localIP, resolvedHost)
}
}

Expand Down Expand Up @@ -325,6 +341,39 @@ func parseHosts(h string, resolveDNS bool) []string {
return resolved
}

// parseHostPairs parses the host string into hostPair slices. When resolveDNS is true,
// each hostname is resolved to its IPs and each IP becomes a separate pair carrying the
// original hostname so that S3 signing and SNI remain correct.
func parseHostPairs(h string, resolveDNS bool) []hostPair {
raw := parseHosts(h, false)
if !resolveDNS {
pairs := make([]hostPair, len(raw))
for i, r := range raw {
pairs[i] = hostPair{resolved: r}
}
return pairs
}
var pairs []hostPair
for _, hostport := range raw {
host, port, _ := net.SplitHostPort(hostport)
if host == "" {
host = hostport
}
ips, err := net.LookupIP(host)
if err != nil {
fatalIf(probe.NewError(err), "Could not get IPs for "+hostport)
}
for _, ip := range ips {
resolved := ip.String()
if port != "" {
resolved = ip.String() + ":" + port
}
pairs = append(pairs, hostPair{resolved: resolved, originalHost: hostport})
}
}
return pairs
}

// mustGetSystemCertPool - return system CAs or empty pool in case of error (or windows)
func mustGetSystemCertPool() *x509.CertPool {
rootCAs, err := certs.GetRootCAs("")
Expand All @@ -338,15 +387,19 @@ func mustGetSystemCertPool() *x509.CertPool {
}

func newAdminClient(ctx *cli.Context) *madmin.AdminClient {
hosts := parseHosts(ctx.String("host"), ctx.Bool("resolve-host"))
if len(hosts) == 0 {
pairs := parseHostPairs(ctx.String("host"), ctx.Bool("resolve-host"))
if len(pairs) == 0 {
fatalIf(probe.NewError(errors.New("no host defined")), "Unable to create MinIO admin client")
}

cl, err := madmin.NewWithOptions(hosts[0], &madmin.Options{
endpoint := pairs[0].resolved
if pairs[0].originalHost != "" {
endpoint = pairs[0].originalHost
}
cl, err := madmin.NewWithOptions(endpoint, &madmin.Options{
Creds: credentials.NewStaticV4(ctx.String("access-key"), ctx.String("secret-key"), ""),
Secure: ctx.Bool("tls") || ctx.Bool("ktls"),
Transport: clientTransport(ctx),
Transport: clientTransportWithLocalIP(ctx, "", pairs[0].resolved, pairs[0].originalHost),
})
fatalIf(probe.NewError(err), "Unable to create MinIO admin client")
cl.SetAppInfo(appName, pkg.Version)
Expand Down
6 changes: 5 additions & 1 deletion cli/client_default.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ import (
"github.com/minio/cli"
)

func clientTransportDefault(ctx *cli.Context, localIP string) http.RoundTripper {
func clientTransportDefault(ctx *cli.Context, localIP, resolvedHost string) http.RoundTripper {
dialer := makeDialer(localIP)
if resolvedHost != "" {
return newClientTransport(ctx, withResolveHost(resolvedHost, resolvedHost, dialer, false))
}
return newClientTransport(ctx, withLocalAddr(localIP))
}
47 changes: 42 additions & 5 deletions cli/client_ktls.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
package cli

import (
"context"
"net"
stdHttp "net/http"
"os"
"time"
Expand All @@ -27,7 +29,15 @@ import (
"gitlab.com/go-extension/tls"
)

func clientTransportKTLS(ctx *cli.Context, localIP string) stdHttp.RoundTripper {
func clientTransportKTLS(ctx *cli.Context, localIP, resolvedHost, originalHost string) stdHttp.RoundTripper {
var sni string
if originalHost != "" {
if h, _, err := net.SplitHostPort(originalHost); err == nil {
sni = h
} else {
sni = originalHost
}
}
// Keep TLS config.
tlsConfig := &tls.Config{
RootCAs: mustGetSystemCertPool(),
Expand All @@ -36,6 +46,7 @@ func clientTransportKTLS(ctx *cli.Context, localIP string) stdHttp.RoundTripper
// Can't use TLSv1.1 because of RC4 cipher usage
MinVersion: tls.VersionTLS12,
InsecureSkipVerify: ctx.Bool("insecure"),
ServerName: sni,
ClientSessionCache: tls.NewLRUClientSessionCache(1024), // up to 1024 nodes

// Extra configs
Expand All @@ -54,16 +65,42 @@ func clientTransportKTLS(ctx *cli.Context, localIP string) stdHttp.RoundTripper

netD := makeDialer(localIP)

getDialAddr := func(addr string) string {
if originalHost == "" || resolvedHost == "" {
return addr
}
host, port, err := net.SplitHostPort(addr)
if err != nil {
host = addr
port = "443"
}
targetHost, _, err := net.SplitHostPort(resolvedHost)
if err != nil {
targetHost = resolvedHost
}
if host != targetHost {
return net.JoinHostPort(targetHost, port)
}
return addr
}

// If we don't enable http/2, then using a custom DialTLSConext is the best choice.
// It can improve performance by not using a compatibility layer.
if !ctx.Bool("http2") {
dialer := &tls.Dialer{NetDialer: netD, Config: tlsConfig}
return newClientTransport(ctx, withDialTLSContext(dialer.DialContext))
tlsDialer := &tls.Dialer{NetDialer: netD, Config: tlsConfig}
h1Dialer := func(ctx context.Context, network, addr string) (net.Conn, error) {
dialAddr := getDialAddr(addr)
return tlsDialer.DialContext(ctx, network, dialAddr)
}
return newClientTransport(ctx, withDialTLSContext(h1Dialer))
}

tr := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: netD.DialContext,
Proxy: http.ProxyFromEnvironment,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
dialAddr := getDialAddr(addr)
return netD.DialContext(ctx, network, dialAddr)
},
MaxIdleConnsPerHost: ctx.Int("concurrent"),
WriteBufferSize: ctx.Int("sndbuf"), // Configure beyond 4KiB default buffer size.
ReadBufferSize: ctx.Int("rcvbuf"), // Configure beyond 4KiB default buffer size.
Expand Down
21 changes: 19 additions & 2 deletions cli/client_tls.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,22 @@ package cli

import (
"crypto/tls"
"net"
"net/http"
"os"

"github.com/minio/cli"
)

func clientTransportTLS(ctx *cli.Context, localIP string) http.RoundTripper {
func clientTransportTLS(ctx *cli.Context, localIP, resolvedHost, originalHost string) http.RoundTripper {
var sni string
if originalHost != "" {
if h, _, err := net.SplitHostPort(originalHost); err == nil {
sni = h
} else {
sni = originalHost
}
}
// Keep TLS config.
tlsConfig := &tls.Config{
RootCAs: mustGetSystemCertPool(),
Expand All @@ -34,12 +43,20 @@ func clientTransportTLS(ctx *cli.Context, localIP string) http.RoundTripper {
// Can't use TLSv1.1 because of RC4 cipher usage
MinVersion: tls.VersionTLS12,
InsecureSkipVerify: ctx.Bool("insecure"),
ServerName: sni,
Comment thread
maniche1024 marked this conversation as resolved.
ClientSessionCache: tls.NewLRUClientSessionCache(1024), // up to 1024 nodes
}

if ctx.Bool("debug") {
tlsConfig.KeyLogWriter = os.Stdout
}

return newClientTransport(ctx, withTLSConfig(tlsConfig), withLocalAddr(localIP))
dialer := makeDialer(localIP)
opts := []transportOption{withTLSConfig(tlsConfig)}
if originalHost != "" {
opts = append(opts, withResolveHost(resolvedHost, originalHost, dialer, true))
} else {
opts = append(opts, withLocalAddr(localIP))
}
return newClientTransport(ctx, opts...)
}
30 changes: 30 additions & 0 deletions cli/client_transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,36 @@ func withDialTLSContext(dialer func(ctx context.Context, network, addr string) (
}
}

// withResolveHost rewrites the dial address from the logical hostname to the
// resolved IP when --resolve-host is active. Only activates when originalHost != "".
// Proxy connections are not rewritten (if addr doesn't match our target, it's a proxy).
func withResolveHost(resolvedHost, originalHost string, dialer *net.Dialer, isTLS bool) transportOption {
return func(transport *http.Transport) {
if originalHost == "" || resolvedHost == "" {
return
}
targetHost, _, err := net.SplitHostPort(resolvedHost)
if err != nil {
targetHost = resolvedHost
}
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
host, port, err := net.SplitHostPort(addr)
if err != nil {
host = addr
port = "80"
if isTLS {
port = "443"
}
}
dialAddr := addr
if host != targetHost {
dialAddr = net.JoinHostPort(targetHost, port)
}
return dialer.DialContext(ctx, network, dialAddr)
}
}
}

func newClientTransport(ctx *cli.Context, options ...transportOption) http.RoundTripper {
tr := &http.Transport{
Proxy: http.ProxyFromEnvironment,
Expand Down
2 changes: 1 addition & 1 deletion cli/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ var ioFlags = []cli.Flag{
},
cli.BoolFlag{
Name: "resolve-host",
Usage: "Resolve the host(s) ip(s) (including multiple A/AAAA records). This can break SSL certificates, use --insecure if so",
Usage: "Resolve the host(s) ip(s) (including multiple A/AAAA records)",
Hidden: true,
},
cli.IntFlag{
Expand Down
Loading