From 0556e6cfb0ff040e8b8dac8d13607619c99e8879 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E9=87=91=E5=9D=A4?= Date: Wed, 2 Sep 2026 10:53:40 +0800 Subject: [PATCH 1/4] Restrict server-side remote import targets --- cmd/octobus/main.go | 2 +- internal/packageimport/git_source.go | 8 +- internal/packageimport/importer.go | 40 +--- internal/packageimport/remote_security.go | 179 ++++++++++++++++++ .../packageimport/remote_security_test.go | 59 ++++++ 5 files changed, 254 insertions(+), 34 deletions(-) create mode 100644 internal/packageimport/remote_security.go create mode 100644 internal/packageimport/remote_security_test.go diff --git a/cmd/octobus/main.go b/cmd/octobus/main.go index cba29da2..8b0c4ed2 100644 --- a/cmd/octobus/main.go +++ b/cmd/octobus/main.go @@ -125,7 +125,7 @@ func serve(opts serveOptions) error { if err := startupInventory(ctx, logger, st); err != nil { return err } - adminServer := &admin.Server{Store: st, Importer: &packageimport.Importer{DataDir: dataDir, Store: st}, Supervisor: sup, Gateway: gateway, AccessLogPath: filepath.Join(dataDir, accesslog.FileName), Logger: logger} + adminServer := &admin.Server{Store: st, Importer: &packageimport.Importer{DataDir: dataDir, Store: st, RemoteTargetValidator: packageimport.DefaultRemoteTargetValidator}, Supervisor: sup, Gateway: gateway, AccessLogPath: filepath.Join(dataDir, accesslog.FileName), Logger: logger} grpcServer := protocol.GRPCServer(gateway) publicServer := admin.NewHTTPServer(opts.addr, h2c.NewHandler(server.CombinedHandler(adminServer.Handler(), grpcServer, gateway), &http2.Server{})) publicListener, err := net.Listen("tcp", opts.addr) diff --git a/internal/packageimport/git_source.go b/internal/packageimport/git_source.go index 853ef5d8..272fd312 100644 --- a/internal/packageimport/git_source.go +++ b/internal/packageimport/git_source.go @@ -227,6 +227,11 @@ func (i *Importer) prepareGitSource(ctx context.Context, rawSource, staging stri if err := os.MkdirAll(repoDir, 0o755); err != nil { return preparedSource{}, err } + if i.RemoteTargetValidator != nil { + if err := i.RemoteTargetValidator(ctx, src.CredentialURL); err != nil { + return preparedSource{}, fmt.Errorf("validate Git remote: %w", err) + } + } if err := runner.run(ctx, repoDir, "init", "--bare", "."); err != nil { return preparedSource{}, err } @@ -318,7 +323,8 @@ func (r *gitRunner) run(ctx context.Context, dir string, args ...string) error { } func (r *gitRunner) output(ctx context.Context, dir string, args ...string) (string, error) { - cmd := exec.CommandContext(ctx, "git", args...) + gitArgs := append([]string{"-c", "http.followRedirects=false"}, args...) + cmd := exec.CommandContext(ctx, "git", gitArgs...) cmd.Dir = dir cmd.Env = r.env var out strings.Builder diff --git a/internal/packageimport/importer.go b/internal/packageimport/importer.go index c685e7b5..8a974bbc 100644 --- a/internal/packageimport/importer.go +++ b/internal/packageimport/importer.go @@ -11,7 +11,6 @@ import ( "fmt" "io" "io/fs" - "net/http" "net/url" "os" "os/exec" @@ -27,6 +26,11 @@ import ( type Importer struct { DataDir string Store *store.Store + + // RemoteTargetValidator is configured by the daemon to enforce the + // network policy for server-side remote imports. Tests and local-only + // importers may leave it unset. + RemoteTargetValidator func(context.Context, string) error } type Options struct { @@ -608,7 +612,7 @@ func (i *Importer) prepareSource(ctx context.Context, opts Options, staging stri prepared.ServiceRoot = serviceRoot return prepared, nil case sourceRemoteArchive: - return prepareRemoteArchiveSource(ctx, source, serviceRoot, staging) + return i.prepareRemoteArchiveSource(ctx, source, serviceRoot, staging) case sourceHTTPSGit: return i.prepareGitSource(ctx, opts.Source, staging) case sourceUnsupportedGit: @@ -771,13 +775,13 @@ func hashFile(path string) (string, error) { return domain.HashBytes(b), nil } -func prepareRemoteArchiveSource(ctx context.Context, source, serviceRoot, staging string) (preparedSource, error) { +func (i *Importer) prepareRemoteArchiveSource(ctx context.Context, source, serviceRoot, staging string) (preparedSource, error) { artifactName, err := remoteArchiveArtifactName(source) if err != nil { return preparedSource{}, err } artifactPath := filepath.Join(staging, artifactName) - if err := downloadRemoteArchive(ctx, source, artifactPath); err != nil { + if err := downloadRemoteArchive(ctx, source, artifactPath, i.RemoteTargetValidator); err != nil { return preparedSource{}, err } packageDir := filepath.Join(staging, "package") @@ -819,34 +823,6 @@ func remoteArchiveArtifactName(source string) (string, error) { return "", fmt.Errorf("unsupported remote package source %q: must end with .tgz, .tar.gz, or .zip", redactedRemoteArchiveSource(source)) } -func downloadRemoteArchive(ctx context.Context, source, artifactPath string) error { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, source, nil) - if err != nil { - return fmt.Errorf("download remote package %q: %w", redactedRemoteArchiveSource(source), err) - } - resp, err := http.DefaultClient.Do(req) - if err != nil { - return fmt.Errorf("download remote package %q: %w", redactedRemoteArchiveSource(source), err) - } - defer resp.Body.Close() - if resp.StatusCode < 200 || resp.StatusCode >= 300 { - return fmt.Errorf("download remote package %q: HTTP %d", redactedRemoteArchiveSource(source), resp.StatusCode) - } - if err := os.MkdirAll(filepath.Dir(artifactPath), 0o755); err != nil { - return err - } - out, err := os.OpenFile(artifactPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o644) - if err != nil { - return err - } - _, copyErr := io.Copy(out, resp.Body) - closeErr := out.Close() - if copyErr != nil { - return copyErr - } - return closeErr -} - func redactedRemoteArchiveSource(source string) string { u, err := url.Parse(source) if err != nil { diff --git a/internal/packageimport/remote_security.go b/internal/packageimport/remote_security.go new file mode 100644 index 00000000..cb284f94 --- /dev/null +++ b/internal/packageimport/remote_security.go @@ -0,0 +1,179 @@ +package packageimport + +import ( + "context" + "errors" + "fmt" + "io" + "net" + "net/http" + "net/url" + "os" + "path/filepath" + "time" +) + +const remoteImportTimeout = 10 * time.Minute + +var forbiddenRemoteNetworks = mustParseRemoteNetworks( + "0.0.0.0/8", + "10.0.0.0/8", + "100.64.0.0/10", + "127.0.0.0/8", + "169.254.0.0/16", + "172.16.0.0/12", + "192.0.0.0/24", + "192.0.2.0/24", + "192.168.0.0/16", + "198.18.0.0/15", + "198.51.100.0/24", + "203.0.113.0/24", + "224.0.0.0/4", + "240.0.0.0/4", + "::/128", + "::1/128", + "fc00::/7", + "fe80::/10", + "ff00::/8", + "2001:db8::/32", +) + +func mustParseRemoteNetworks(cidrs ...string) []*net.IPNet { + networks := make([]*net.IPNet, 0, len(cidrs)) + for _, cidr := range cidrs { + _, network, err := net.ParseCIDR(cidr) + if err != nil { + panic(err) + } + networks = append(networks, network) + } + return networks +} + +func DefaultRemoteTargetValidator(ctx context.Context, raw string) error { + u, err := url.Parse(raw) + if err != nil { + return fmt.Errorf("invalid remote URL: %w", err) + } + if u.Scheme != "http" && u.Scheme != "https" { + return errors.New("only HTTP(S) remote targets are allowed") + } + if u.Hostname() == "" { + return errors.New("remote URL host is required") + } + return validateRemoteHost(ctx, u.Hostname()) +} + +func validateRemoteHost(ctx context.Context, hostname string) error { + if ip := net.ParseIP(hostname); ip != nil { + if isForbiddenRemoteIP(ip) { + return fmt.Errorf("remote target %q resolves to a private or special address", hostname) + } + return nil + } + + ips, err := net.DefaultResolver.LookupIPAddr(ctx, hostname) + if err != nil { + return fmt.Errorf("resolve remote target %q: %w", hostname, err) + } + if len(ips) == 0 { + return fmt.Errorf("remote target %q has no address", hostname) + } + for _, item := range ips { + if isForbiddenRemoteIP(item.IP) { + return fmt.Errorf("remote target %q resolves to a private or special address", hostname) + } + } + return nil +} + +func isForbiddenRemoteIP(ip net.IP) bool { + if ip == nil || !ip.IsGlobalUnicast() { + return true + } + for _, network := range forbiddenRemoteNetworks { + if network.Contains(ip) { + return true + } + } + return false +} + +func newRemoteHTTPClient(validate func(context.Context, string) error) *http.Client { + transport := http.DefaultTransport.(*http.Transport).Clone() + client := &http.Client{ + Transport: transport, + Timeout: remoteImportTimeout, + } + if validate == nil { + return client + } + transport.DialContext = safeRemoteDialContext(validate) + client.CheckRedirect = func(req *http.Request, _ []*http.Request) error { + return validate(req.Context(), req.URL.String()) + } + return client +} + +func safeRemoteDialContext(validate func(context.Context, string) error) func(context.Context, string, string) (net.Conn, error) { + dialer := &net.Dialer{} + return func(ctx context.Context, network, address string) (net.Conn, error) { + host, port, err := net.SplitHostPort(address) + if err != nil { + return nil, err + } + if err := validate(ctx, "https://"+net.JoinHostPort(host, port)); err != nil { + return nil, err + } + + ips, err := net.DefaultResolver.LookupIPAddr(ctx, host) + if err != nil { + return nil, err + } + for _, item := range ips { + if isForbiddenRemoteIP(item.IP) { + continue + } + conn, err := dialer.DialContext(ctx, network, net.JoinHostPort(item.IP.String(), port)) + if err == nil { + return conn, nil + } + } + return nil, fmt.Errorf("unable to connect to allowed address for %s", host) + } +} + +func downloadRemoteArchive(ctx context.Context, source, artifactPath string, validate func(context.Context, string) error) error { + if validate != nil { + if err := validate(ctx, source); err != nil { + return fmt.Errorf("validate remote package target: %w", err) + } + } + ctx, cancel := context.WithTimeout(ctx, remoteImportTimeout) + defer cancel() + req, err := http.NewRequestWithContext(ctx, http.MethodGet, source, nil) + if err != nil { + return fmt.Errorf("download remote package %q: %w", redactedRemoteArchiveSource(source), err) + } + resp, err := newRemoteHTTPClient(validate).Do(req) + if err != nil { + return fmt.Errorf("download remote package %q: %w", redactedRemoteArchiveSource(source), err) + } + defer resp.Body.Close() + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return fmt.Errorf("download remote package %q: HTTP %d", redactedRemoteArchiveSource(source), resp.StatusCode) + } + if err := os.MkdirAll(filepath.Dir(artifactPath), 0o755); err != nil { + return err + } + out, err := os.OpenFile(artifactPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o644) + if err != nil { + return err + } + _, copyErr := io.Copy(out, resp.Body) + closeErr := out.Close() + if copyErr != nil { + return copyErr + } + return closeErr +} diff --git a/internal/packageimport/remote_security_test.go b/internal/packageimport/remote_security_test.go new file mode 100644 index 00000000..0c91dfcf --- /dev/null +++ b/internal/packageimport/remote_security_test.go @@ -0,0 +1,59 @@ +package packageimport + +import ( + "context" + "net/http" + "net/url" + "strings" + "testing" +) + +func TestDefaultRemoteTargetValidatorRejectsPrivateAndSpecialAddresses(t *testing.T) { + for _, raw := range []string{ + "http://127.0.0.1/package.tgz", + "https://localhost/package.tgz", + "http://169.254.169.254/package.tgz", + "http://192.0.2.8/package.tgz", + "http://[::1]/package.tgz", + } { + err := DefaultRemoteTargetValidator(context.Background(), raw) + if err == nil { + t.Fatalf("validator accepted %s", raw) + } + if !strings.Contains(err.Error(), "private or special") { + t.Fatalf("unexpected error for %s: %v", raw, err) + } + } +} + +func TestDefaultRemoteTargetValidatorRejectsUnsupportedURLs(t *testing.T) { + if err := DefaultRemoteTargetValidator(context.Background(), "file:///tmp/package.tgz"); err == nil { + t.Fatal("validator accepted a non-HTTP URL") + } +} + +func TestRemoteHTTPClientRevalidatesRedirects(t *testing.T) { + validator := func(_ context.Context, raw string) error { + if strings.HasSuffix(raw, "/internal") { + return context.Canceled + } + return nil + } + client := newRemoteHTTPClient(validator) + redirect, err := url.Parse("https://public.example/internal") + if err != nil { + t.Fatal(err) + } + err = client.CheckRedirect(&http.Request{URL: redirect, Method: http.MethodGet}, nil) + if err == nil || !strings.Contains(err.Error(), context.Canceled.Error()) { + t.Fatalf("redirect validation error = %v", err) + } +} + +func TestPrepareGitSourceRejectsPrivateRemoteBeforeGitFetch(t *testing.T) { + imp := &Importer{RemoteTargetValidator: DefaultRemoteTargetValidator} + _, err := imp.prepareGitSource(context.Background(), "https://127.0.0.1/repo.git", t.TempDir()) + if err == nil || !strings.Contains(err.Error(), "private or special") { + t.Fatalf("private Git remote error = %v", err) + } +} From cbcd635dc298f92ce407c0a4668e968d1fa71b8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E9=87=91=E5=9D=A4?= Date: Thu, 3 Sep 2026 01:29:38 +0800 Subject: [PATCH 2/4] Pin Git remote connections through validation proxy --- internal/packageimport/git_source.go | 31 ++++-- internal/packageimport/remote_security.go | 101 ++++++++++++++++++ .../packageimport/remote_security_test.go | 11 ++ 3 files changed, 137 insertions(+), 6 deletions(-) diff --git a/internal/packageimport/git_source.go b/internal/packageimport/git_source.go index 272fd312..d1eabe7f 100644 --- a/internal/packageimport/git_source.go +++ b/internal/packageimport/git_source.go @@ -219,7 +219,17 @@ func (i *Importer) prepareGitSource(ctx context.Context, rawSource, staging stri if err != nil { return preparedSource{}, err } - runner, err := newGitRunner(src, staging) + var gitProxy *validatedGitProxy + var proxyURL string + if i.RemoteTargetValidator != nil { + gitProxy, err = startValidatedGitProxy(ctx) + if err != nil { + return preparedSource{}, fmt.Errorf("start Git validation proxy: %w", err) + } + defer gitProxy.Close() + proxyURL = gitProxy.URL() + } + runner, err := newGitRunner(src, staging, proxyURL) if err != nil { return preparedSource{}, err } @@ -276,11 +286,12 @@ func serviceRootOrDefault(serviceRoot string) string { } type gitRunner struct { - source gitSource - env []string + source gitSource + env []string + proxyURL string } -func newGitRunner(src gitSource, staging string) (*gitRunner, error) { +func newGitRunner(src gitSource, staging string, proxyURL ...string) (*gitRunner, error) { if _, err := exec.LookPath("git"); err != nil { return nil, errors.New("git is required to import HTTPS Git sources; install git and ensure it is on PATH") } @@ -299,7 +310,11 @@ func newGitRunner(src gitSource, staging string) (*gitRunner, error) { } else { env = append(env, "GIT_TERMINAL_PROMPT=0") } - return &gitRunner{source: src, env: env}, nil + runner := &gitRunner{source: src, env: env} + if len(proxyURL) > 0 { + runner.proxyURL = proxyURL[0] + } + return runner, nil } func writeGitAskpass(staging string, src gitSource) (string, error) { @@ -323,7 +338,11 @@ func (r *gitRunner) run(ctx context.Context, dir string, args ...string) error { } func (r *gitRunner) output(ctx context.Context, dir string, args ...string) (string, error) { - gitArgs := append([]string{"-c", "http.followRedirects=false"}, args...) + gitArgs := []string{"-c", "http.followRedirects=false"} + if r.proxyURL != "" { + gitArgs = append(gitArgs, "-c", "http.proxy="+r.proxyURL) + } + gitArgs = append(gitArgs, args...) cmd := exec.CommandContext(ctx, "git", gitArgs...) cmd.Dir = dir cmd.Env = r.env diff --git a/internal/packageimport/remote_security.go b/internal/packageimport/remote_security.go index cb284f94..c564b801 100644 --- a/internal/packageimport/remote_security.go +++ b/internal/packageimport/remote_security.go @@ -1,6 +1,7 @@ package packageimport import ( + "bufio" "context" "errors" "fmt" @@ -10,6 +11,7 @@ import ( "net/url" "os" "path/filepath" + "sync" "time" ) @@ -108,6 +110,9 @@ func newRemoteHTTPClient(validate func(context.Context, string) error) *http.Cli if validate == nil { return client } + // Do not let HTTP(S)_PROXY redirect a validated request to an + // unvalidated destination through an external proxy. + transport.Proxy = nil transport.DialContext = safeRemoteDialContext(validate) client.CheckRedirect = func(req *http.Request, _ []*http.Request) error { return validate(req.Context(), req.URL.String()) @@ -143,6 +148,102 @@ func safeRemoteDialContext(validate func(context.Context, string) error) func(co } } +// validatedGitProxy forces the git subprocess to use the target policy while +// retaining the original hostname for HTTPS certificate verification. +type validatedGitProxy struct { + listener net.Listener + ctx context.Context + done chan struct{} + closeOnce sync.Once +} + +func startValidatedGitProxy(ctx context.Context) (*validatedGitProxy, error) { + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return nil, err + } + proxy := &validatedGitProxy{listener: listener, ctx: ctx, done: make(chan struct{})} + go proxy.serve() + return proxy, nil +} + +func (p *validatedGitProxy) URL() string { + return "http://" + p.listener.Addr().String() +} + +func (p *validatedGitProxy) serve() { + defer close(p.done) + go func() { + select { + case <-p.ctx.Done(): + _ = p.listener.Close() + case <-p.done: + } + }() + for { + conn, err := p.listener.Accept() + if err != nil { + return + } + go p.handle(conn) + } +} + +func (p *validatedGitProxy) handle(client net.Conn) { + defer client.Close() + request, err := http.ReadRequest(bufio.NewReader(client)) + if err != nil || request.Method != http.MethodConnect { + _, _ = io.WriteString(client, "HTTP/1.1 405 Method Not Allowed\\r\\nConnection: close\\r\\n\\r\\n") + return + } + remote, err := dialValidatedRemote(p.ctx, request.Host) + if err != nil { + _, _ = io.WriteString(client, "HTTP/1.1 403 Forbidden\\r\\nConnection: close\\r\\n\\r\\n") + return + } + defer remote.Close() + if _, err := io.WriteString(client, "HTTP/1.1 200 Connection Established\\r\\n\\r\\n"); err != nil { + return + } + go func() { + _, _ = io.Copy(remote, client) + _ = remote.Close() + }() + _, _ = io.Copy(client, remote) +} + +func (p *validatedGitProxy) Close() error { + var err error + p.closeOnce.Do(func() { err = p.listener.Close() }) + <-p.done + return err +} + +func dialValidatedRemote(ctx context.Context, address string) (net.Conn, error) { + host, port, err := net.SplitHostPort(address) + if err != nil { + return nil, err + } + if err := DefaultRemoteTargetValidator(ctx, "https://"+net.JoinHostPort(host, port)); err != nil { + return nil, err + } + ips, err := net.DefaultResolver.LookupIPAddr(ctx, host) + if err != nil { + return nil, err + } + dialer := &net.Dialer{} + for _, item := range ips { + if isForbiddenRemoteIP(item.IP) { + continue + } + conn, err := dialer.DialContext(ctx, "tcp", net.JoinHostPort(item.IP.String(), port)) + if err == nil { + return conn, nil + } + } + return nil, fmt.Errorf("unable to connect to allowed address for %s", host) +} + func downloadRemoteArchive(ctx context.Context, source, artifactPath string, validate func(context.Context, string) error) error { if validate != nil { if err := validate(ctx, source); err != nil { diff --git a/internal/packageimport/remote_security_test.go b/internal/packageimport/remote_security_test.go index 0c91dfcf..caad8be5 100644 --- a/internal/packageimport/remote_security_test.go +++ b/internal/packageimport/remote_security_test.go @@ -32,6 +32,17 @@ func TestDefaultRemoteTargetValidatorRejectsUnsupportedURLs(t *testing.T) { } } +func TestRemoteHTTPClientDisablesAmbientProxy(t *testing.T) { + client := newRemoteHTTPClient(DefaultRemoteTargetValidator) + transport, ok := client.Transport.(*http.Transport) + if !ok { + t.Fatalf("transport type = %T", client.Transport) + } + if transport.Proxy != nil { + t.Fatal("validated remote client inherited an ambient proxy") + } +} + func TestRemoteHTTPClientRevalidatesRedirects(t *testing.T) { validator := func(_ context.Context, raw string) error { if strings.HasSuffix(raw, "/internal") { From b9214c378d7b1bff3e81ef26869c8062c6ff54c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E9=87=91=E5=9D=A4?= Date: Thu, 3 Sep 2026 01:40:20 +0800 Subject: [PATCH 3/4] Honor configured remote validation in Git proxy --- internal/packageimport/git_source.go | 2 +- internal/packageimport/remote_security.go | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/internal/packageimport/git_source.go b/internal/packageimport/git_source.go index d1eabe7f..fd134015 100644 --- a/internal/packageimport/git_source.go +++ b/internal/packageimport/git_source.go @@ -222,7 +222,7 @@ func (i *Importer) prepareGitSource(ctx context.Context, rawSource, staging stri var gitProxy *validatedGitProxy var proxyURL string if i.RemoteTargetValidator != nil { - gitProxy, err = startValidatedGitProxy(ctx) + gitProxy, err = startValidatedGitProxy(ctx, i.RemoteTargetValidator) if err != nil { return preparedSource{}, fmt.Errorf("start Git validation proxy: %w", err) } diff --git a/internal/packageimport/remote_security.go b/internal/packageimport/remote_security.go index c564b801..997296f2 100644 --- a/internal/packageimport/remote_security.go +++ b/internal/packageimport/remote_security.go @@ -153,16 +153,17 @@ func safeRemoteDialContext(validate func(context.Context, string) error) func(co type validatedGitProxy struct { listener net.Listener ctx context.Context + validate func(context.Context, string) error done chan struct{} closeOnce sync.Once } -func startValidatedGitProxy(ctx context.Context) (*validatedGitProxy, error) { +func startValidatedGitProxy(ctx context.Context, validate func(context.Context, string) error) (*validatedGitProxy, error) { listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { return nil, err } - proxy := &validatedGitProxy{listener: listener, ctx: ctx, done: make(chan struct{})} + proxy := &validatedGitProxy{listener: listener, ctx: ctx, validate: validate, done: make(chan struct{})} go proxy.serve() return proxy, nil } @@ -196,7 +197,7 @@ func (p *validatedGitProxy) handle(client net.Conn) { _, _ = io.WriteString(client, "HTTP/1.1 405 Method Not Allowed\\r\\nConnection: close\\r\\n\\r\\n") return } - remote, err := dialValidatedRemote(p.ctx, request.Host) + remote, err := dialValidatedRemote(p.ctx, request.Host, p.validate) if err != nil { _, _ = io.WriteString(client, "HTTP/1.1 403 Forbidden\\r\\nConnection: close\\r\\n\\r\\n") return @@ -219,12 +220,15 @@ func (p *validatedGitProxy) Close() error { return err } -func dialValidatedRemote(ctx context.Context, address string) (net.Conn, error) { +func dialValidatedRemote(ctx context.Context, address string, validate func(context.Context, string) error) (net.Conn, error) { host, port, err := net.SplitHostPort(address) if err != nil { return nil, err } - if err := DefaultRemoteTargetValidator(ctx, "https://"+net.JoinHostPort(host, port)); err != nil { + if validate == nil { + validate = DefaultRemoteTargetValidator + } + if err := validate(ctx, "https://"+net.JoinHostPort(host, port)); err != nil { return nil, err } ips, err := net.DefaultResolver.LookupIPAddr(ctx, host) From dc37109cead6ccf5a9c47a9039092ce5016ec020 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E9=87=91=E5=9D=A4?= Date: Thu, 3 Sep 2026 03:39:04 +0800 Subject: [PATCH 4/4] Preserve buffered bytes in Git proxy tunnels --- internal/packageimport/remote_security.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/packageimport/remote_security.go b/internal/packageimport/remote_security.go index 997296f2..16e169aa 100644 --- a/internal/packageimport/remote_security.go +++ b/internal/packageimport/remote_security.go @@ -192,7 +192,8 @@ func (p *validatedGitProxy) serve() { func (p *validatedGitProxy) handle(client net.Conn) { defer client.Close() - request, err := http.ReadRequest(bufio.NewReader(client)) + reader := bufio.NewReader(client) + request, err := http.ReadRequest(reader) if err != nil || request.Method != http.MethodConnect { _, _ = io.WriteString(client, "HTTP/1.1 405 Method Not Allowed\\r\\nConnection: close\\r\\n\\r\\n") return @@ -207,7 +208,7 @@ func (p *validatedGitProxy) handle(client net.Conn) { return } go func() { - _, _ = io.Copy(remote, client) + _, _ = io.Copy(remote, io.MultiReader(reader, client)) _ = remote.Close() }() _, _ = io.Copy(client, remote)