Initial DDC release
This commit is contained in:
commit
434f6441da
57 files changed
+4606
No files matched your search
Executable
+54
@@ -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
|
||||
}
|
||||
Executable
+55
@@ -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
|
||||
}
|
||||
Executable
+98
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user