Initial DDC release

This commit is contained in:
SirBlob committed 2026-10-25 20:24:50 -04:00
commit 434f6441da
57 files changed
+4606

No files matched your search

+54
View File
@@ -0,0 +1,54 @@
package guard
import "net/netip"
var blockedPrefixes = []netip.Prefix{
netip.MustParsePrefix("0.0.0.0/8"),
netip.MustParsePrefix("10.0.0.0/8"),
netip.MustParsePrefix("100.64.0.0/10"),
netip.MustParsePrefix("127.0.0.0/8"),
netip.MustParsePrefix("169.254.0.0/16"),
netip.MustParsePrefix("172.16.0.0/12"),
netip.MustParsePrefix("192.0.0.0/24"),
netip.MustParsePrefix("192.0.2.0/24"),
netip.MustParsePrefix("192.88.99.0/24"),
netip.MustParsePrefix("192.168.0.0/16"),
netip.MustParsePrefix("198.18.0.0/15"),
netip.MustParsePrefix("198.51.100.0/24"),
netip.MustParsePrefix("203.0.113.0/24"),
netip.MustParsePrefix("224.0.0.0/4"),
netip.MustParsePrefix("240.0.0.0/4"),
netip.MustParsePrefix("::/128"),
netip.MustParsePrefix("::1/128"),
netip.MustParsePrefix("::ffff:0:0/96"),
netip.MustParsePrefix("64:ff9b::/96"),
netip.MustParsePrefix("64:ff9b:1::/48"),
netip.MustParsePrefix("100::/64"),
netip.MustParsePrefix("2001::/32"),
netip.MustParsePrefix("2001:db8::/32"),
netip.MustParsePrefix("2002::/16"),
netip.MustParsePrefix("fc00::/7"),
netip.MustParsePrefix("fe80::/10"),
netip.MustParsePrefix("fec0::/10"),
netip.MustParsePrefix("ff00::/8"),
}
func IsPublicAddress(address netip.Addr) bool {
address = address.Unmap().WithZone("")
if !address.IsValid() ||
!address.IsGlobalUnicast() ||
address.IsPrivate() ||
address.IsLoopback() ||
address.IsLinkLocalUnicast() ||
address.IsMulticast() ||
address.IsUnspecified() {
return false
}
for _, prefix := range blockedPrefixes {
if prefix.Contains(address) {
return false
}
}
return true
}
+55
View File
@@ -0,0 +1,55 @@
package guard
import (
"net"
"net/http"
"net/netip"
"syscall"
"time"
)
func (policy *Policy) NewHTTPClient() *http.Client {
dialer := &net.Dialer{
Timeout: 15 * time.Second,
KeepAlive: 30 * time.Second,
Control: policy.controlConnection,
}
transport := &http.Transport{
Proxy: nil,
DialContext: dialer.DialContext,
ForceAttemptHTTP2: true,
TLSHandshakeTimeout: 15 * time.Second,
ResponseHeaderTimeout: 30 * time.Second,
ExpectContinueTimeout: time.Second,
MaxIdleConns: 10,
IdleConnTimeout: 90 * time.Second,
}
return &http.Client{
Transport: transport,
CheckRedirect: policy.checkRedirect,
}
}
func (policy *Policy) controlConnection(_ string, address string, _ syscall.RawConn) error {
addressPort, err := netip.ParseAddrPort(address)
if err != nil {
return ErrBlockedAddress
}
if !IsPublicAddress(addressPort.Addr()) {
return ErrBlockedAddress
}
if !policy.portAllowed(addressPort.Port()) {
return ErrBlockedPort
}
return nil
}
func (policy *Policy) checkRedirect(request *http.Request, previous []*http.Request) error {
if len(previous) >= maxRedirects {
return ErrTooManyRedirects
}
_, err := policy.ValidateURL(request.URL.String())
return err
}
+98
View File
@@ -0,0 +1,98 @@
package guard
import (
"errors"
"fmt"
"net/netip"
"net/url"
"strconv"
"strings"
"sync"
)
const (
maxURLLength = 4096
maxRedirects = 5
)
var (
ErrURLTooLong = errors.New("URL is too long")
ErrUnsupportedScheme = errors.New("only http and https URLs are allowed")
ErrMissingHost = errors.New("URL must include a host")
ErrBlockedAddress = errors.New("destination address is not allowed")
ErrBlockedPort = errors.New("destination port is not allowed")
ErrTooManyRedirects = errors.New("too many redirects")
)
type Policy struct {
mutex sync.RWMutex
allowedPorts map[uint16]bool
}
func NewPolicy(allowedPorts []uint16) *Policy {
policy := &Policy{}
policy.SetAllowedPorts(allowedPorts)
return policy
}
func (policy *Policy) SetAllowedPorts(allowedPorts []uint16) {
ports := make(map[uint16]bool, len(allowedPorts))
for _, port := range allowedPorts {
ports[port] = true
}
policy.mutex.Lock()
defer policy.mutex.Unlock()
policy.allowedPorts = ports
}
func (policy *Policy) ValidateURL(raw string) (*url.URL, error) {
raw = strings.TrimSpace(raw)
if len(raw) > maxURLLength {
return nil, ErrURLTooLong
}
parsed, err := url.Parse(raw)
if err != nil {
return nil, fmt.Errorf("invalid URL: %w", err)
}
if parsed.Scheme != "http" && parsed.Scheme != "https" {
return nil, ErrUnsupportedScheme
}
host := parsed.Hostname()
if host == "" {
return nil, ErrMissingHost
}
if address, err := netip.ParseAddr(host); err == nil && !IsPublicAddress(address) {
return nil, ErrBlockedAddress
}
port, err := portOf(parsed)
if err != nil {
return nil, err
}
if !policy.portAllowed(port) {
return nil, ErrBlockedPort
}
return parsed, nil
}
func (policy *Policy) portAllowed(port uint16) bool {
policy.mutex.RLock()
defer policy.mutex.RUnlock()
return len(policy.allowedPorts) == 0 || policy.allowedPorts[port]
}
func portOf(parsed *url.URL) (uint16, error) {
if raw := parsed.Port(); raw != "" {
value, err := strconv.ParseUint(raw, 10, 16)
if err != nil {
return 0, fmt.Errorf("invalid port: %w", err)
}
return uint16(value), nil
}
if parsed.Scheme == "https" {
return 443, nil
}
return 80, nil
}