Initial commit: Null DRM Official

Capture, decrypt, and restream toolkit with compiled-in app modules
(RTE, TG4, BBC), on-device MITM proxy, streamd control plane, and www.
BBC module.yaml is published (clear streams); other module values stay local.
This commit is contained in:
404errordeveloper 2026-10-06 00:25:35 +02:00
commit 2fa8f2435f
121 changed files with 17802 additions and 0 deletions

Binary file not shown.

View file

@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"
GO="${GO:-$(command -v go || true)}"
if [[ -z "$GO" && -x /opt/homebrew/bin/go ]]; then
GO=/opt/homebrew/bin/go
fi
if [[ -z "$GO" ]]; then
echo "go not found; brew install go" >&2
exit 1
fi
echo "building linux/arm64 with $GO"
OUT="${OUT:-../proxy-android-arm64}"
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 "$GO" build -ldflags='-s -w' -o "$OUT" .
file "$OUT"
ls -lh "$OUT"
# Prefer: from repo root → bin/proxyctl.exe build

14
apps/proxy/device/go.mod Normal file
View file

@ -0,0 +1,14 @@
module appproxy
go 1.27.1
require (
github.com/andybalholm/brotli v1.0.6 // indirect
github.com/elazarl/goproxy v1.9.2 // indirect
github.com/klauspost/compress v1.17.4 // indirect
github.com/refraction-networking/utls v1.8.2 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.50.0 // indirect
golang.org/x/sys v0.48.0 // indirect
golang.org/x/text v0.34.0 // indirect
)

16
apps/proxy/device/go.sum Normal file
View file

@ -0,0 +1,16 @@
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/elazarl/goproxy v1.9.2 h1:+vXRRSWrznMtBrAb559qfqC+Cny1Q3rR0l51Yu/3WUw=
github.com/elazarl/goproxy v1.9.2/go.mod h1:THdE5ix2clxX9lZzcICPpZ67d6CdrPZxdOYsNgU5e30=
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo=
github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=

1310
apps/proxy/device/main.go Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,56 @@
package main
import "strings"
// CA identity and file names. The legacy names are still honoured when already
// present on a device, so an existing phone does not need a CA reinject.
const (
caCertName = "appproxy-ca.crt"
caKeyName = "appproxy-ca.key"
legacyCACertName = "rteproxy-ca.crt"
legacyCAKeyName = "rteproxy-ca.key"
caOrganization = "appproxy MITM CA"
caCommonName = "appproxy"
)
// stringList is a repeatable string flag.
type stringList []string
func (s *stringList) String() string { return strings.Join(*s, ",") }
func (s *stringList) Set(v string) error {
for _, part := range strings.Split(v, ",") {
part = strings.TrimSpace(part)
if part != "" {
*s = append(*s, part)
}
}
return nil
}
var (
// matchNeedles (--match) mark extra provider traffic as interesting.
matchNeedles stringList
// scoreHosts (--score-host) are this provider's manifest origins.
scoreHosts stringList
// scoreDeny (--score-deny) are provider URLs that are never a manifest.
scoreDeny stringList
// mitmHosts (--mitm-host) force MITM even when a passthrough rule matches.
mitmHosts stringList
)
// formatDeny are catalog/analytics URL shapes that are never a manifest for any
// provider. Provider-specific noise arrives via --score-deny.
var formatDeny = []string{
"schedules", "bylistingtime", "maxlistings", "bycallsign",
"/feed.", "playback_config", "config.json",
}
func denyNeedles() []string {
out := make([]string, 0, len(formatDeny)+len(scoreDeny))
out = append(out, formatDeny...)
for _, d := range scoreDeny {
out = append(out, strings.ToLower(d))
}
return out
}

View file

@ -0,0 +1,101 @@
package main
import (
"io"
"log"
"net"
"strconv"
"strings"
"sync"
)
// passthroughAll, when true, splices every host without MITM. Used to prove
// transparent redirect + VPN egress work before narrowing MITM targets.
var passthroughAll bool
// passthroughDefault, when true (transparent mode), MITM only forceMITM / open.live.
// Avoids breaking connectivity checks on Google/Firebase when Magisk CA is not
// in the app mount namespace.
var passthroughDefault bool
// forceMITM hosts (lowercase) always MITM even if a passthrough rule matches.
// Use for known license endpoints once identified: -mitm-host license.example.com
var forceMITM []string
// Hosts that must NOT be MITM'd for playback to work (CDN / media / BBC APIs).
// Transparent mode defaults to passthrough; carve in with open.live / -mitm-host.
func shouldPassthrough(host string) bool {
if passthroughAll {
return true
}
h := strings.ToLower(stripHostPort(host))
if h == "" {
return false
}
for _, m := range forceMITM {
if h == m || strings.HasSuffix(h, "."+m) {
return false
}
}
// No SNI → only have a destination IP; MITM cert/SNI would be wrong.
if ip := net.ParseIP(h); ip != nil {
return true
}
// MITM open.live (mediaselector) — stream + Widevine licence URLs live here.
// Upstream MUST dial SO_ORIGINAL_DST (VPN fake-IP) or BBC returns geolocation 403.
if h == "open.live.bbc.co.uk" {
return false
}
// Other BBC APIs/CDNs: MITM breaks play; passthrough.
if strings.Contains(h, "bbc.co.uk") || strings.Contains(h, "bbci.co.uk") ||
strings.Contains(h, "bbc.com") || strings.Contains(h, "bbci.com") {
return true
}
needles := []string{
"akamai", "akamaized", "cloudfront.net", "fastly",
"edgesuite", "cmaf", "2cnt.net", "springstreams",
"fingerprint", "optimizely", "appsflyer", "urbanairship",
"googleusercontent", "gvt1.com", "googleapis.com",
"firebaselogging", "crashlytics", "app-measurement",
}
for _, n := range needles {
if strings.Contains(h, n) {
return true
}
}
if passthroughDefault {
return true
}
return false
}
func handlePassthrough(client net.Conn, host string, port int, dial func(network, addr string) (net.Conn, error)) {
defer client.Close()
target := net.JoinHostPort(host, strconv.Itoa(port))
up, err := dial("tcp", target)
if err != nil {
log.Printf("[PASS] dial %s: %v", target, err)
return
}
defer up.Close()
log.Printf("[PASS] %s (no MITM)", target)
errc := make(chan error, 2)
var once sync.Once
closeWrite := func(c net.Conn) {
once.Do(func() {})
if tc, ok := c.(*net.TCPConn); ok {
_ = tc.CloseWrite()
}
}
go func() {
_, err := io.Copy(up, client)
errc <- err
closeWrite(up)
}()
go func() {
_, err := io.Copy(client, up)
errc <- err
closeWrite(client)
}()
<-errc
}

41
apps/proxy/device/push.sh Normal file
View file

@ -0,0 +1,41 @@
#!/usr/bin/env bash
# Push rteproxy to the phone and start it in the background.
set -euo pipefail
cd "$(dirname "$0")"
BIN="${1:-rteproxy-android-arm64}"
REMOTE="${REMOTE:-/data/local/tmp/rteproxy}"
OUT="${OUT:-/data/local/tmp/rte_cap.json}"
LISTEN="${LISTEN:-:8080}"
DNS="${DNS:-1.1.1.1,1.0.0.1,8.8.8.8}"
ADB="${ADB:-adb}"
if [[ ! -f "$BIN" ]]; then
echo "missing $BIN — run ./build.sh first" >&2
exit 1
fi
"$ADB" get-state >/dev/null
"$ADB" push "$BIN" "$REMOTE"
"$ADB" shell chmod 755 "$REMOTE"
# Stop a previous instance (ignore if none)
"$ADB" shell "pkill -f /data/local/tmp/rteproxy" >/dev/null 2>&1 || true
sleep 0.5
"$ADB" shell "sh -c '$REMOTE -listen $LISTEN -out $OUT -ca-dir /data/local/tmp -dns $DNS >/data/local/tmp/rteproxy.log 2>&1 &'"
sleep 1
echo "--- process ---"
"$ADB" shell "ps -A | grep rteproxy || true"
echo "--- log ---"
"$ADB" shell "cat /data/local/tmp/rteproxy.log || true"
echo
echo "CA cert on device: /data/local/tmp/rteproxy-ca.crt"
echo " adb pull /data/local/tmp/rteproxy-ca.crt ."
echo " → Settings → Security → Install a certificate → CA certificate"
echo
echo "Set Wi‑Fi HTTP proxy to 127.0.0.1${LISTEN}"
echo "Play the stream, then:"
echo " adb pull $OUT /tmp/rte_cap.json"
echo " python getrtelive.py"

View file

@ -0,0 +1,81 @@
#!/system/bin/sh
# Transparent redirect: package UID TCP/443 → local appproxy (no Wi‑Fi http_proxy).
# Usage:
# tproxy_iptables.sh start <package> [port]
# tproxy_iptables.sh stop
# tproxy_iptables.sh status
set -eu
CHAIN=APPROXY_TPROXY
ACTION="${1:-}"
uid_for_package() {
pkg="$1"
# dumpsys package <pkg> | grep userId= OR stat on data dir
uid=$(dumpsys package "$pkg" 2>/dev/null | grep -m1 -oE 'userId=[0-9]+' | head -1 | cut -d= -f2 || true)
if [ -z "$uid" ]; then
uid=$(stat -c %u "/data/user/0/$pkg" 2>/dev/null || true)
fi
echo "$uid"
}
stop_rules() {
iptables -t nat -D OUTPUT -j "$CHAIN" 2>/dev/null || true
iptables -t nat -F "$CHAIN" 2>/dev/null || true
iptables -t nat -X "$CHAIN" 2>/dev/null || true
echo "STOPPED"
}
start_rules() {
pkg="$1"
port="$2"
uid=$(uid_for_package "$pkg")
if [ -z "$uid" ] || [ "$uid" = "0" ]; then
echo "ERR: cannot resolve uid for package $pkg" >&2
exit 1
fi
proxy_uid=$(stat -c %u /data/local/tmp/appproxy 2>/dev/null || echo "")
# Prefer the running process uid if available
if pidof appproxy >/dev/null 2>&1; then
proxy_uid=$(stat -c %u /proc/$(pidof appproxy | awk '{print $1}') 2>/dev/null || echo "$proxy_uid")
fi
stop_rules >/dev/null
iptables -t nat -N "$CHAIN"
# Never redirect the mitm itself (loop).
if [ -n "$proxy_uid" ]; then
iptables -t nat -A "$CHAIN" -m owner --uid-owner "$proxy_uid" -j RETURN
fi
# Skip localhost / link-local.
iptables -t nat -A "$CHAIN" -d 127.0.0.0/8 -j RETURN
iptables -t nat -A "$CHAIN" -d 10.0.0.0/8 -j RETURN 2>/dev/null || true
# Redirect only the target app's HTTPS.
iptables -t nat -A "$CHAIN" -p tcp -m owner --uid-owner "$uid" --dport 443 -j REDIRECT --to-ports "$port"
iptables -t nat -A OUTPUT -j "$CHAIN"
echo "STARTED pkg=$pkg uid=$uid port=$port proxy_uid=${proxy_uid:-unknown}"
}
status_rules() {
echo "=== $CHAIN ==="
iptables -t nat -L "$CHAIN" -n -v 2>/dev/null || echo "(no chain)"
echo "=== OUTPUT head ==="
iptables -t nat -L OUTPUT -n -v 2>/dev/null | head -20
}
case "$ACTION" in
start)
pkg="${2:?package required}"
port="${3:-8080}"
start_rules "$pkg" "$port"
;;
stop)
stop_rules
;;
status)
status_rules
;;
*)
echo "usage: $0 start <package> [port] | stop | status" >&2
exit 2
;;
esac

View file

@ -0,0 +1,382 @@
package main
import (
"bufio"
"encoding/binary"
"errors"
"fmt"
"io"
"log"
"net"
"strconv"
"strings"
"sync"
"time"
"unsafe"
"golang.org/x/sys/unix"
)
// dialFunc is the DNS-aware upstream dialer (same as MITM transport DialContext).
type dialFunc func(network, addr string) (net.Conn, error)
// serveTransparentAccept accepts iptables-REDIRECTED TCP connections, recovers
// the original destination / SNI, then feeds them into the local HTTP MITM via
// a synthetic CONNECT so UK-VPN routing stays intact (no Wi‑Fi http_proxy).
func serveTransparentAccept(ln net.Listener, mitmAddr string, dial dialFunc) {
log.Printf("transparent accept on %s → MITM %s", ln.Addr(), mitmAddr)
for {
c, err := ln.Accept()
if err != nil {
if errors.Is(err, net.ErrClosed) {
return
}
log.Printf("transparent accept: %v", err)
continue
}
go handleTransparent(c, mitmAddr, dial)
}
}
func handleTransparent(client net.Conn, mitmAddr string, dial dialFunc) {
defer client.Close()
_ = client.SetDeadline(time.Now().Add(30 * time.Second))
bc := newBufConn(client)
sni, helloErr := peekSNI(bc)
dstHost, dstPort, dstErr := originalDst(client)
host := strings.TrimSpace(sni)
port := 443
if dstErr == nil && dstPort > 0 {
port = dstPort
}
if host == "" {
if dstErr != nil {
log.Printf("transparent: no SNI (%v) and no original dst (%v)", helloErr, dstErr)
return
}
host = dstHost
}
target := net.JoinHostPort(host, strconv.Itoa(port))
// CDN / media / mediaselector: splice without MITM so playback works.
if shouldPassthrough(host) {
_ = client.SetDeadline(time.Time{})
// Re-feed ClientHello: peekSNI left bytes in bc; rebuild a reader.
left := bc.buffered()
var clientR net.Conn = client
if len(left) > 0 {
clientR = &prefixConn{Conn: client, prefix: left}
}
upDial := dial
if upDial == nil {
upDial = func(network, addr string) (net.Conn, error) {
return net.DialTimeout(network, addr, 15*time.Second)
}
}
// Prefer the app's original destination IP (same CDN edge / geo) over re-resolve.
passHost := host
if dstErr == nil && net.ParseIP(dstHost) != nil && !hostIsLoopback(dstHost) {
passHost = dstHost
log.Printf("[PASS] %s → %s:%d (orig-dst)", host, dstHost, port)
}
handlePassthrough(clientR, passHost, port, upDial)
return
}
log.Printf("[TPROXY] %s → CONNECT %s", client.RemoteAddr(), target)
mitm, err := net.DialTimeout("tcp", mitmAddr, 5*time.Second)
if err != nil {
log.Printf("transparent dial mitm: %v", err)
return
}
defer mitm.Close()
// Pass SO_ORIGINAL_DST so MITM upstream dials the app's IP (NordVPN fake-IP
// / same CDN edge). Re-resolving via public DNS breaks BBC geolocation.
var b strings.Builder
fmt.Fprintf(&b, "CONNECT %s HTTP/1.1\r\nHost: %s\r\n", target, target)
if dstErr == nil && net.ParseIP(dstHost) != nil && !hostIsLoopback(dstHost) {
fmt.Fprintf(&b, "X-Appproxy-Orig-Dst: %s\r\n", net.JoinHostPort(dstHost, strconv.Itoa(port)))
}
b.WriteString("\r\n")
if _, err := io.WriteString(mitm, b.String()); err != nil {
log.Printf("transparent CONNECT write: %v", err)
return
}
br := bufio.NewReader(mitm)
status, err := br.ReadString('\n')
if err != nil {
log.Printf("transparent CONNECT read: %v", err)
return
}
if !strings.Contains(status, "200") {
rest, _ := io.ReadAll(io.LimitReader(br, 512))
log.Printf("transparent CONNECT rejected: %s%s", status, rest)
return
}
// Drain remaining response headers.
for {
line, err := br.ReadString('\n')
if err != nil || line == "\r\n" || line == "\n" {
break
}
}
_ = client.SetDeadline(time.Time{})
_ = mitm.SetDeadline(time.Time{})
// Any buffered ClientHello bytes must go to the MITM first.
var once sync.Once
left := bc.buffered()
if len(left) > 0 {
if _, err := mitm.Write(left); err != nil {
log.Printf("transparent hello write: %v", err)
return
}
}
errc := make(chan error, 2)
go func() {
_, err := io.Copy(mitm, bc)
errc <- err
once.Do(func() {
if tc, ok := mitm.(*net.TCPConn); ok {
_ = tc.CloseWrite()
}
})
}()
go func() {
_, err := io.Copy(client, br)
errc <- err
if tc, ok := client.(*net.TCPConn); ok {
_ = tc.CloseWrite()
}
}()
<-errc
}
// prefixConn emits prefix once, then reads from Conn.
type prefixConn struct {
net.Conn
prefix []byte
i int
}
func (p *prefixConn) Read(b []byte) (int, error) {
if p.i < len(p.prefix) {
n := copy(b, p.prefix[p.i:])
p.i += n
return n, nil
}
return p.Conn.Read(b)
}
type bufConn struct {
net.Conn
r *bufio.Reader
}
func newBufConn(c net.Conn) *bufConn {
return &bufConn{Conn: c, r: bufio.NewReaderSize(c, 4096)}
}
func (b *bufConn) Read(p []byte) (int, error) { return b.r.Read(p) }
func (b *bufConn) buffered() []byte {
n := b.r.Buffered()
if n == 0 {
return nil
}
buf, _ := b.r.Peek(n)
out := make([]byte, len(buf))
copy(out, buf)
// Consume so later Read does not duplicate.
_, _ = b.r.Discard(n)
return out
}
// peekSNI reads a TLS ClientHello (via Peek) and returns the SNI hostname.
func peekSNI(bc *bufConn) (string, error) {
hdr, err := bc.r.Peek(5)
if err != nil {
return "", err
}
if hdr[0] != 0x16 { // handshake
return "", fmt.Errorf("not TLS handshake (type=%d)", hdr[0])
}
recLen := int(hdr[3])<<8 | int(hdr[4])
need := 5 + recLen
if need > 16*1024 {
return "", fmt.Errorf("ClientHello too large (%d)", need)
}
// Wait until full record is buffered.
deadline := time.Now().Add(5 * time.Second)
for bc.r.Buffered() < need && time.Now().Before(deadline) {
_ = bc.Conn.SetReadDeadline(time.Now().Add(200 * time.Millisecond))
_, _ = bc.r.Peek(need)
}
_ = bc.Conn.SetReadDeadline(time.Time{})
if bc.r.Buffered() < need {
need = bc.r.Buffered()
}
data, err := bc.r.Peek(need)
if err != nil {
return "", err
}
return parseClientHelloSNI(data)
}
func parseClientHelloSNI(rec []byte) (string, error) {
if len(rec) < 5+4 {
return "", fmt.Errorf("short record")
}
if rec[0] != 0x16 || rec[5] != 0x01 { // handshake + client_hello
return "", fmt.Errorf("not ClientHello")
}
// Skip: record hdr(5) + hs type(1) + hs len(3) + client version(2) + random(32)
i := 5 + 1 + 3 + 2 + 32
if i >= len(rec) {
return "", fmt.Errorf("truncated ClientHello")
}
// session id
sidLen := int(rec[i])
i += 1 + sidLen
if i+2 > len(rec) {
return "", fmt.Errorf("truncate cipher suites")
}
csLen := int(rec[i])<<8 | int(rec[i+1])
i += 2 + csLen
if i+1 > len(rec) {
return "", fmt.Errorf("truncate compression")
}
compLen := int(rec[i])
i += 1 + compLen
if i+2 > len(rec) {
return "", nil // no extensions
}
extLen := int(rec[i])<<8 | int(rec[i+1])
i += 2
end := i + extLen
if end > len(rec) {
end = len(rec)
}
for i+4 <= end {
typ := int(rec[i])<<8 | int(rec[i+1])
l := int(rec[i+2])<<8 | int(rec[i+3])
i += 4
if i+l > end {
break
}
if typ == 0 { // server_name
return parseSNIExtension(rec[i : i+l])
}
i += l
}
return "", nil
}
func parseSNIExtension(b []byte) (string, error) {
if len(b) < 2 {
return "", nil
}
listLen := int(b[0])<<8 | int(b[1])
i := 2
end := 2 + listLen
if end > len(b) {
end = len(b)
}
for i+3 <= end {
nameType := b[i]
nameLen := int(b[i+1])<<8 | int(b[i+2])
i += 3
if i+nameLen > end {
break
}
if nameType == 0 {
return string(b[i : i+nameLen]), nil
}
i += nameLen
}
return "", nil
}
func originalDst(conn net.Conn) (host string, port int, err error) {
tcp, ok := conn.(*net.TCPConn)
if !ok {
return "", 0, fmt.Errorf("not TCP")
}
rc, err := tcp.SyscallConn()
if err != nil {
return "", 0, err
}
var (
ip net.IP
prt int
err2 error
)
cerr := rc.Control(func(fd uintptr) {
ip, prt, err2 = getOrigDstFD(int(fd))
})
if cerr != nil {
return "", 0, cerr
}
if err2 != nil {
return "", 0, err2
}
return ip.String(), prt, nil
}
func getOrigDstFD(fd int) (net.IP, int, error) {
// Try IPv6 structure first (works for IPv4-mapped too on many kernels).
if ip, port, err := getOrigDstIPv6(fd); err == nil {
return ip, port, nil
}
return getOrigDstIPv4(fd)
}
func getOrigDstIPv4(fd int) (net.IP, int, error) {
const soOriginalDst = 80
var addr unix.RawSockaddrInet4
sz := uint32(unsafe.Sizeof(addr))
_, _, errno := unix.Syscall6(
unix.SYS_GETSOCKOPT,
uintptr(fd),
uintptr(unix.IPPROTO_IP),
uintptr(soOriginalDst),
uintptr(unsafe.Pointer(&addr)),
uintptr(unsafe.Pointer(&sz)),
0,
)
if errno != 0 {
return nil, 0, errno
}
ip := net.IPv4(addr.Addr[0], addr.Addr[1], addr.Addr[2], addr.Addr[3])
port := int(binary.BigEndian.Uint16((*[2]byte)(unsafe.Pointer(&addr.Port))[:]))
return ip, port, nil
}
func getOrigDstIPv6(fd int) (net.IP, int, error) {
const soOriginalDst = 80
var addr unix.RawSockaddrInet6
sz := uint32(unsafe.Sizeof(addr))
_, _, errno := unix.Syscall6(
unix.SYS_GETSOCKOPT,
uintptr(fd),
uintptr(unix.IPPROTO_IPV6),
uintptr(soOriginalDst),
uintptr(unsafe.Pointer(&addr)),
uintptr(unsafe.Pointer(&sz)),
0,
)
if errno != 0 {
return nil, 0, errno
}
ip := make(net.IP, 16)
copy(ip, addr.Addr[:])
port := int(binary.BigEndian.Uint16((*[2]byte)(unsafe.Pointer(&addr.Port))[:]))
return ip, port, nil
}

View file

@ -0,0 +1,77 @@
package main
import (
"context"
"crypto/tls"
"fmt"
"net"
"strings"
"time"
utls "github.com/refraction-networking/utls"
)
// dialTLSChrome dials addr with a Chrome-like ClientHello so CDNs (Fastly)
// are less likely to HTTP 403 Go's default TLS fingerprint.
// Handshakes as HTTP/1.1 only so net/http can use the returned conn.
func dialTLSChrome(ctx context.Context, dialCtx func(context.Context, string, string) (net.Conn, error), network, addr string) (net.Conn, error) {
host := serverNameFromAddr(addr)
raw, err := dialCtx(ctx, network, addr)
if err != nil {
return nil, err
}
cfg := &utls.Config{
ServerName: host,
InsecureSkipVerify: true,
NextProtos: []string{"http/1.1"},
}
spec, err := utls.UTLSIdToSpec(utls.HelloChrome_120)
if err != nil {
_ = raw.Close()
return dialTLSStd(ctx, dialCtx, network, addr)
}
for i := range spec.Extensions {
if alpn, ok := spec.Extensions[i].(*utls.ALPNExtension); ok {
alpn.AlpnProtocols = []string{"http/1.1"}
}
}
uConn := utls.UClient(raw, cfg, utls.HelloCustom)
if err := uConn.ApplyPreset(&spec); err != nil {
_ = raw.Close()
return nil, fmt.Errorf("utls preset %s: %w", host, err)
}
deadline, ok := ctx.Deadline()
if !ok {
deadline = time.Now().Add(15 * time.Second)
}
_ = raw.SetDeadline(deadline)
if err := uConn.Handshake(); err != nil {
_ = raw.Close()
return nil, fmt.Errorf("utls handshake %s: %w", host, err)
}
_ = raw.SetDeadline(time.Time{})
return uConn, nil
}
func dialTLSStd(ctx context.Context, dialCtx func(context.Context, string, string) (net.Conn, error), network, addr string) (net.Conn, error) {
host := serverNameFromAddr(addr)
raw, err := dialCtx(ctx, network, addr)
if err != nil {
return nil, err
}
cfg := &tls.Config{ServerName: host, InsecureSkipVerify: true, NextProtos: []string{"http/1.1"}}
c := tls.Client(raw, cfg)
if err := c.HandshakeContext(ctx); err != nil {
_ = raw.Close()
return nil, err
}
return c, nil
}
func serverNameFromAddr(addr string) string {
host, _, err := net.SplitHostPort(addr)
if err != nil {
return strings.TrimSpace(addr)
}
return host
}