Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support customized DNS resolving for remote registry #696

Merged
merged 17 commits into from
Dec 23, 2022
70 changes: 65 additions & 5 deletions cmd/oras/internal/option/remote.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"net"
"net/http"
"os"
"strconv"
"strings"
"time"

Expand All @@ -35,15 +36,24 @@ import (
"oras.land/oras/internal/version"
)

type ResolveEntry struct {
qweeah marked this conversation as resolved.
Show resolved Hide resolved
from string
to net.IP
port int
}

// Remote options struct.
type Remote struct {
resolveFlag []string
qweeah marked this conversation as resolved.
Show resolved Hide resolved

CACertFilePath string
PlainHTTP bool
Insecure bool
Configs []string
Username string
PasswordFromStdin bool
Password string
Resolves []*ResolveEntry
qweeah marked this conversation as resolved.
Show resolved Hide resolved
qweeah marked this conversation as resolved.
Show resolved Hide resolved
}

// ApplyFlags applies flags to a command flag set.
Expand Down Expand Up @@ -76,6 +86,10 @@ func (opts *Remote) ApplyFlagsWithPrefix(fs *pflag.FlagSet, prefix, description
if fs.Lookup("registry-config") == nil {
fs.StringArrayVarP(&opts.Configs, "registry-config", "", nil, "`path` of the authentication file")
}

if fs.Lookup("resolve") == nil {
fs.StringArrayVarP(&opts.resolveFlag, "resolve", "", nil, "customized DNS formatted in `host:port:address`")
}
}

// ReadPassword tries to read password with optional cmd prompt.
Expand All @@ -94,6 +108,34 @@ func (opts *Remote) ReadPassword() (err error) {
return nil
}

// ParseResolve parses resolve flag.
func (opts *Remote) ParseResolve() (err error) {
errorMsg := "failed to parse resolve flag %q: %s"
qweeah marked this conversation as resolved.
Show resolved Hide resolved
for _, r := range opts.resolveFlag {
parts := strings.SplitN(r, ":", 3)
if len(parts) < 3 {
return fmt.Errorf(errorMsg, r, "expecting host:port:address")
}

port, err := strconv.ParseInt(parts[1], 10, 64)
if err != nil {
return fmt.Errorf(errorMsg, r, "expecting uint64 port")
}

// ipv6 zone is not parsed
to := net.ParseIP(parts[2])
if to == nil {
return fmt.Errorf(errorMsg, r, "invalid IP address")
}
opts.Resolves = append(opts.Resolves, &ResolveEntry{
from: parts[0],
port: int(port),
Fixed Show fixed Hide fixed
to: to,
})
}
return nil
}

// tlsConfig assembles the tls config.
func (opts *Remote) tlsConfig() (*tls.Config, error) {
config := &tls.Config{
Expand All @@ -109,6 +151,27 @@ func (opts *Remote) tlsConfig() (*tls.Config, error) {
return config, nil
}

var defaultDialer = &net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}

// DialContext connects to the addr on the named network using
// the provided context.
func (opts *Remote) DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
var matched *ResolveEntry
for _, r := range opts.Resolves {
if addr == fmt.Sprintf("%s:%d", r.from, r.port) {
matched = r
break
}
}
if matched == nil {
return defaultDialer.DialContext(ctx, network, addr)
}
return net.DialTCP(network, nil, &net.TCPAddr{IP: matched.to, Port: matched.port})
}
qweeah marked this conversation as resolved.
Show resolved Hide resolved

// authClient assembles a oras auth client.
func (opts *Remote) authClient(registry string, debug bool) (client *auth.Client, err error) {
config, err := opts.tlsConfig()
Expand All @@ -119,11 +182,8 @@ func (opts *Remote) authClient(registry string, debug bool) (client *auth.Client
Client: &http.Client{
// default value are derived from http.DefaultTransport
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
Proxy: http.ProxyFromEnvironment,
DialContext: opts.DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
Expand Down