174 lines
4.3 KiB
Go
174 lines
4.3 KiB
Go
package netproxy
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// IPv4 fallback (tony on Discord: models.dev and the update check failed
|
|
// with a bare EOF, direct or not, while curl got through). Where a TUN
|
|
// (xray, Clash's) takes every connection but can't carry IPv6 out, a
|
|
// connection to a site's IPv6 address is accepted, so the dialer's own
|
|
// fallback to IPv4 never starts, and closed once TLS begins. When a
|
|
// connection to an IPv6 address ends before a byte comes back, its
|
|
// host:port is dialed over IPv4 for a while, and the request that met it
|
|
// is sent again once, over IPv4, when nothing of it can have reached the
|
|
// site: an HTTPS request (no byte came back, so TLS never finished) or an
|
|
// idempotent one, and its body can be had again. The request goes as it
|
|
// was; connections over IPv4, and through a proxy, are left as they are.
|
|
|
|
// v6Broken is how long a host:port whose IPv6 failed is dialed over IPv4.
|
|
const v6Broken = 30 * time.Minute
|
|
|
|
var v4Only struct {
|
|
sync.Mutex
|
|
at map[string]time.Time // host:port → when its IPv6 failed
|
|
}
|
|
|
|
func v4Since(addr string) time.Time {
|
|
v4Only.Lock()
|
|
defer v4Only.Unlock()
|
|
at := v4Only.at[addr]
|
|
if !at.IsZero() && time.Since(at) > v6Broken {
|
|
delete(v4Only.at, addr)
|
|
return time.Time{}
|
|
}
|
|
return at
|
|
}
|
|
|
|
func markV4(addr string) {
|
|
v4Only.Lock()
|
|
defer v4Only.Unlock()
|
|
if v4Only.at == nil {
|
|
v4Only.at = map[string]time.Time{}
|
|
}
|
|
v4Only.at[addr] = time.Now()
|
|
}
|
|
|
|
// v4Fallback has t dial a host:port whose IPv6 failed over IPv4, and
|
|
// watch connections to an IPv6 address for failing so.
|
|
func v4Fallback(t *http.Transport) {
|
|
dial := t.DialContext
|
|
if dial == nil {
|
|
dial = (&net.Dialer{}).DialContext
|
|
}
|
|
t.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
if network == "tcp" && !v4Since(addr).IsZero() {
|
|
network = "tcp4"
|
|
}
|
|
c, err := dial(ctx, network, addr)
|
|
if err != nil || c == nil {
|
|
return c, err
|
|
}
|
|
if a, ok := c.RemoteAddr().(*net.TCPAddr); ok && a.IP.To4() == nil {
|
|
return &v6Conn{Conn: c, addr: addr}, nil
|
|
}
|
|
return c, nil
|
|
}
|
|
}
|
|
|
|
// v6Conn is a connection to an IPv6 address, marking its host:port for
|
|
// IPv4 if it fails before a byte comes back.
|
|
type v6Conn struct {
|
|
net.Conn
|
|
addr string
|
|
mu sync.Mutex
|
|
read bool // a byte came back
|
|
shut bool // closed on magpie's side
|
|
}
|
|
|
|
func (c *v6Conn) Read(b []byte) (int, error) {
|
|
n, err := c.Conn.Read(b)
|
|
c.seen(n, err)
|
|
return n, err
|
|
}
|
|
|
|
func (c *v6Conn) Write(b []byte) (int, error) {
|
|
n, err := c.Conn.Write(b)
|
|
c.seen(0, err)
|
|
return n, err
|
|
}
|
|
|
|
func (c *v6Conn) Close() error {
|
|
c.mu.Lock()
|
|
c.shut = true
|
|
c.mu.Unlock()
|
|
return c.Conn.Close()
|
|
}
|
|
|
|
func (c *v6Conn) seen(n int, err error) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if c.read {
|
|
return
|
|
}
|
|
if n > 0 {
|
|
c.read = true
|
|
return
|
|
}
|
|
// a timeout, or a close of magpie's own, says nothing of the path
|
|
if err == nil || c.shut || errors.Is(err, net.ErrClosed) || errors.Is(err, os.ErrDeadlineExceeded) {
|
|
return
|
|
}
|
|
c.read = true // once
|
|
markV4(c.addr)
|
|
}
|
|
|
|
// v4Retry is res, err of req, or of req sent again when its first try met
|
|
// an IPv6 address that failed (see v4Fallback) and it can be sent again.
|
|
func v4Retry(t *http.Transport, req *http.Request) (*http.Response, error) {
|
|
start := time.Now()
|
|
res, err := t.RoundTrip(req)
|
|
if err == nil || req.URL == nil {
|
|
return res, err
|
|
}
|
|
if t.Proxy != nil {
|
|
if p, perr := t.Proxy(req); perr != nil || p != nil {
|
|
return res, err
|
|
}
|
|
}
|
|
if req.Context().Err() != nil || v4Since(hostPort(req)).Before(start) {
|
|
return res, err
|
|
}
|
|
if req.URL.Scheme != "https" && !idempotent(req.Method) {
|
|
return res, err
|
|
}
|
|
again := req.Clone(req.Context())
|
|
if req.Body != nil && req.Body != http.NoBody {
|
|
if req.GetBody == nil {
|
|
return res, err
|
|
}
|
|
body, berr := req.GetBody()
|
|
if berr != nil {
|
|
return res, err
|
|
}
|
|
again.Body = body
|
|
}
|
|
return t.RoundTrip(again)
|
|
}
|
|
|
|
func idempotent(m string) bool {
|
|
switch m {
|
|
case "", "GET", "HEAD", "OPTIONS", "TRACE", "PUT", "DELETE":
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// hostPort is the host:port req's connection is dialed to, directly.
|
|
func hostPort(req *http.Request) string {
|
|
if p := req.URL.Port(); p != "" {
|
|
return net.JoinHostPort(req.URL.Hostname(), p)
|
|
}
|
|
port := "80"
|
|
if req.URL.Scheme == "https" {
|
|
port = "443"
|
|
}
|
|
return net.JoinHostPort(req.URL.Hostname(), port)
|
|
}
|