mirror of
https://github.com/langgenius/dify.git
synced 2026-11-12 22:31:24 +08:00
Track backend integration, Ruff cleanup and migration reconciliation in ENG-1271. Retain required copy contracts without implementing frontend interactions.
313 lines
8.2 KiB
Go
313 lines
8.2 KiB
Go
//go:build linux
|
|
|
|
package testscope
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"errors"
|
|
"golang.org/x/sys/unix"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/netip"
|
|
"os"
|
|
"os/exec"
|
|
"strconv"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
)
|
|
|
|
// Private destinations are denied by default. Explicit deployment-owned mediator
|
|
// addresses are the only exception; request payloads cannot add these addresses.
|
|
func AllowedAddress(ip netip.Addr, mediators []netip.Addr) bool {
|
|
ip = ip.Unmap()
|
|
for _, allowed := range mediators {
|
|
if ip == allowed.Unmap() {
|
|
return true
|
|
}
|
|
}
|
|
return ip.IsValid() && ip.IsGlobalUnicast() && !ip.IsPrivate() && !ip.IsLoopback() && !ip.IsLinkLocalUnicast() && !ip.IsUnspecified()
|
|
}
|
|
|
|
type PinnedTarget struct {
|
|
Addresses []string `json:"addresses"`
|
|
MaxBytes int64 `json:"max_bytes"`
|
|
TimeoutSeconds int64 `json:"timeout_seconds"`
|
|
}
|
|
type PinnedNetwork struct {
|
|
Targets map[string]PinnedTarget `json:"targets"`
|
|
}
|
|
|
|
func (p PinnedNetwork) Resolve(authority string) (string, error) {
|
|
target, ok := p.Targets[authority]
|
|
if !ok || len(target.Addresses) == 0 {
|
|
return "", errors.New("unapproved network authority")
|
|
}
|
|
return target.Addresses[0], nil
|
|
}
|
|
func PinNetwork(ctx context.Context, targets []NetworkTarget, mediators []string) (PinnedNetwork, error) {
|
|
pinned := PinnedNetwork{Targets: map[string]PinnedTarget{}}
|
|
for _, target := range targets {
|
|
ips, err := net.DefaultResolver.LookupNetIP(ctx, "ip", target.Host)
|
|
if err != nil || len(ips) == 0 {
|
|
return pinned, errors.New("target resolution failed")
|
|
}
|
|
p := PinnedTarget{MaxBytes: target.MaxBytes, TimeoutSeconds: target.TimeoutSeconds}
|
|
for _, ip := range ips {
|
|
address := net.JoinHostPort(ip.Unmap().String(), strconv.Itoa(int(target.Port)))
|
|
allowed := AllowedAddress(ip, nil)
|
|
for _, mediator := range mediators {
|
|
if address == mediator {
|
|
allowed = true
|
|
}
|
|
}
|
|
if !allowed {
|
|
return pinned, errors.New("target resolves to forbidden network")
|
|
}
|
|
p.Addresses = append(p.Addresses, address)
|
|
}
|
|
pinned.Targets[net.JoinHostPort(target.Host, strconv.Itoa(int(target.Port)))] = p
|
|
}
|
|
return pinned, nil
|
|
}
|
|
|
|
type Broker struct {
|
|
listener net.Listener
|
|
policy PinnedNetwork
|
|
spend func(string, int64) error
|
|
deadline time.Time
|
|
mu sync.Mutex
|
|
conns map[net.Conn]bool
|
|
closed bool
|
|
peerPID int
|
|
slots chan struct{}
|
|
}
|
|
|
|
func StartBroker(socket string, policy PinnedNetwork, deadline time.Time, spend func(string, int64) error) (*Broker, error) {
|
|
listener, err := net.Listen("unix", socket)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
b := &Broker{listener: listener, policy: policy, deadline: deadline, spend: spend, conns: map[net.Conn]bool{}, slots: make(chan struct{}, 4)}
|
|
go func() {
|
|
_ = (&http.Server{Handler: b, ReadHeaderTimeout: 3 * time.Second, MaxHeaderBytes: 16 << 10}).Serve(&peerListener{Listener: listener, broker: b})
|
|
}()
|
|
return b, nil
|
|
}
|
|
func (b *Broker) Close() {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
b.closed = true
|
|
_ = b.listener.Close()
|
|
for conn := range b.conns {
|
|
_ = conn.Close()
|
|
}
|
|
}
|
|
func (b *Broker) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
select {
|
|
case b.slots <- struct{}{}:
|
|
defer func() { <-b.slots }()
|
|
default:
|
|
http.Error(w, "broker connection limit", http.StatusTooManyRequests)
|
|
return
|
|
}
|
|
authority := r.Host
|
|
if r.Method != http.MethodConnect {
|
|
if r.URL.Scheme != "http" {
|
|
http.Error(w, "scheme denied", http.StatusForbidden)
|
|
return
|
|
}
|
|
authority = r.URL.Host
|
|
}
|
|
address, err := b.policy.Resolve(authority)
|
|
if err != nil {
|
|
http.Error(w, "target denied", http.StatusForbidden)
|
|
return
|
|
}
|
|
target := b.policy.Targets[authority]
|
|
deadline := time.Now().Add(time.Duration(target.TimeoutSeconds) * time.Second)
|
|
if b.deadline.Before(deadline) {
|
|
deadline = b.deadline
|
|
}
|
|
if !deadline.After(time.Now()) {
|
|
http.Error(w, "scope expired", http.StatusForbidden)
|
|
return
|
|
}
|
|
ctx, cancel := context.WithDeadline(r.Context(), deadline)
|
|
defer cancel()
|
|
b.mu.Lock()
|
|
if b.closed {
|
|
b.mu.Unlock()
|
|
http.Error(w, "scope revoked", http.StatusForbidden)
|
|
return
|
|
}
|
|
// Charge a conservative connection unit before every dial, including CONNECT.
|
|
if err = b.spend(authority, 1); err != nil {
|
|
b.mu.Unlock()
|
|
http.Error(w, "network budget exhausted", http.StatusBadGateway)
|
|
return
|
|
}
|
|
raw, err := (&net.Dialer{Timeout: time.Second}).DialContext(ctx, "tcp", address)
|
|
if err != nil {
|
|
b.mu.Unlock()
|
|
http.Error(w, "target unavailable", http.StatusBadGateway)
|
|
return
|
|
}
|
|
_ = raw.SetDeadline(deadline)
|
|
b.conns[raw] = true
|
|
b.mu.Unlock()
|
|
defer func() { _ = raw.Close(); b.mu.Lock(); delete(b.conns, raw); b.mu.Unlock() }()
|
|
conn := &budgetConn{Conn: raw, spend: func(n int64) error { return b.spend(authority, n) }}
|
|
if r.Method == http.MethodConnect {
|
|
client, buffer, err := w.(http.Hijacker).Hijack()
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer func() { _ = client.Close() }()
|
|
_ = client.SetDeadline(deadline)
|
|
_, _ = buffer.WriteString("HTTP/1.1 200 Connection Established\r\n\r\n")
|
|
if err = buffer.Flush(); err != nil {
|
|
return
|
|
}
|
|
done := make(chan struct{})
|
|
go func() { _, _ = io.Copy(conn, buffer); _ = raw.Close(); close(done) }()
|
|
_, _ = io.Copy(client, conn)
|
|
_ = client.Close()
|
|
_ = raw.Close()
|
|
<-done
|
|
return
|
|
}
|
|
r.RequestURI = ""
|
|
r.URL.Scheme = ""
|
|
r.URL.Host = ""
|
|
r.Header.Del("Proxy-Authorization")
|
|
r.Header.Del("Proxy-Connection")
|
|
r.Close = true
|
|
if err = r.Write(conn); err != nil {
|
|
http.Error(w, "network budget exceeded", http.StatusBadGateway)
|
|
return
|
|
}
|
|
response, err := http.ReadResponse(bufio.NewReader(conn), r)
|
|
if err != nil {
|
|
http.Error(w, "upstream unavailable", http.StatusBadGateway)
|
|
return
|
|
}
|
|
defer func() { _ = response.Body.Close() }()
|
|
for name, values := range response.Header {
|
|
for _, v := range values {
|
|
w.Header().Add(name, v)
|
|
}
|
|
}
|
|
w.WriteHeader(response.StatusCode)
|
|
_, _ = io.Copy(w, response.Body)
|
|
}
|
|
|
|
type budgetConn struct {
|
|
net.Conn
|
|
spend func(int64) error
|
|
}
|
|
|
|
func (c *budgetConn) Read(p []byte) (int, error) {
|
|
if len(p) == 0 {
|
|
return 0, nil
|
|
}
|
|
// Reserve before consuming upstream bytes. Failed reservations spend zero;
|
|
// shrink conservatively so a small remaining budget can still make progress.
|
|
amount := len(p)
|
|
if amount > 1024 {
|
|
amount = 1024
|
|
}
|
|
for amount > 0 {
|
|
if err := c.spend(int64(amount)); err == nil {
|
|
return c.Conn.Read(p[:amount])
|
|
}
|
|
amount /= 2
|
|
}
|
|
return 0, errors.New("network budget exhausted")
|
|
}
|
|
func (c *budgetConn) Write(p []byte) (int, error) {
|
|
if err := c.spend(int64(len(p))); err != nil {
|
|
return 0, err
|
|
}
|
|
return c.Conn.Write(p)
|
|
}
|
|
|
|
// StartWorkerProxy is a local byte relay. Only this trusted process can open the
|
|
// broker Unix channel; the submitted job's chroot contains no broker socket.
|
|
func StartWorkerProxy(socket string) error {
|
|
if err := exec.Command("ip", "link", "set", "lo", "up").Run(); err != nil {
|
|
return err
|
|
}
|
|
listener, err := net.Listen("tcp", "127.0.0.1:18080")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
slots := make(chan struct{}, 4)
|
|
go func() {
|
|
for {
|
|
client, err := listener.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
select {
|
|
case slots <- struct{}{}:
|
|
default:
|
|
_ = client.Close()
|
|
continue
|
|
}
|
|
_ = client.SetDeadline(time.Now().Add(60 * time.Second))
|
|
go func() {
|
|
defer func() { <-slots }()
|
|
defer func() { _ = client.Close() }()
|
|
broker, err := net.Dial("unix", socket)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer func() { _ = broker.Close() }()
|
|
done := make(chan struct{})
|
|
go func() { _, _ = io.Copy(broker, client); _ = broker.Close(); close(done) }()
|
|
_, _ = io.Copy(client, broker)
|
|
_ = client.Close()
|
|
<-done
|
|
}()
|
|
}
|
|
}()
|
|
return nil
|
|
}
|
|
|
|
func (b *Broker) SetPeer(pid int) { b.mu.Lock(); defer b.mu.Unlock(); b.peerPID = pid }
|
|
|
|
type peerListener struct {
|
|
net.Listener
|
|
broker *Broker
|
|
}
|
|
|
|
func (l *peerListener) Accept() (net.Conn, error) {
|
|
for {
|
|
conn, err := l.Listener.Accept()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
valid := false
|
|
if socket, ok := conn.(syscall.Conn); ok {
|
|
raw, err := socket.SyscallConn()
|
|
if err == nil {
|
|
_ = raw.Control(func(fd uintptr) {
|
|
cred, err := unix.GetsockoptUcred(int(fd), unix.SOL_SOCKET, unix.SO_PEERCRED)
|
|
if err == nil {
|
|
l.broker.mu.Lock()
|
|
valid = l.broker.peerPID > 0 && int(cred.Pid) == l.broker.peerPID && int(cred.Uid) == os.Getuid()
|
|
l.broker.mu.Unlock()
|
|
}
|
|
})
|
|
}
|
|
}
|
|
if valid {
|
|
return conn, nil
|
|
}
|
|
_ = conn.Close()
|
|
}
|
|
}
|