Files
sing-box/transport/http/connect_udp.go
T
2026-11-09 15:17:20 +00:00

104 lines
3.6 KiB
Go

package http
import (
std_bufio "bufio"
"context"
"net"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/sagernet/sing-box/transport/v2rayhttp"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
const (
connectUDPProtocol = "connect-udp"
connectUDPPathPrefix = "/.well-known/masque/udp/"
)
func parseConnectUDPTarget(path string) (M.Socksaddr, bool) {
rest, found := strings.CutPrefix(path, connectUDPPathPrefix)
if !found {
return M.Socksaddr{}, false
}
segments := strings.Split(strings.TrimSuffix(rest, "/"), "/")
if len(segments) != 2 {
return M.Socksaddr{}, false
}
host, err := url.PathUnescape(segments[0])
if err != nil || host == "" {
return M.Socksaddr{}, false
}
port, err := strconv.ParseUint(segments[1], 10, 16)
if err != nil || port == 0 {
return M.Socksaddr{}, false
}
destination := M.ParseSocksaddrHostPort(host, uint16(port)).Unwrap()
if !destination.IsValid() {
return M.Socksaddr{}, false
}
return destination, true
}
func connectUDPURL(destination M.Socksaddr) *url.URL {
host := destination.AddrString()
port := "/" + strconv.Itoa(int(destination.Port)) + "/"
return &url.URL{
Path: connectUDPPathPrefix + host + port,
RawPath: connectUDPPathPrefix + strings.ReplaceAll(url.PathEscape(host), ":", "%3A") + port,
}
}
func requestIsConnectUDP(request *http.Request) bool {
return request.Method == http.MethodGet && requestIsUpgrade(request) && strings.EqualFold(request.Header.Get("Upgrade"), connectUDPProtocol)
}
func (c *serverConn) serveConnectUDP(ctx context.Context, request *http.Request, source M.Socksaddr) (requestResult, error) {
destination, valid := parseConnectUDPTarget(request.URL.EscapedPath())
if !valid || !request.ProtoAtLeast(1, 1) {
return c.reject(request, requestKeepAlive(request), http.StatusBadRequest, nil, E.New("invalid connect-udp request: ", request.URL.Path))
}
_, err := c.conn.Write([]byte("HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: connect-udp\r\nCapsule-Protocol: ?1\r\n\r\n"))
if err != nil {
return requestClose, E.Cause(err, "write response")
}
c.handler.NewPacketConnectionEx(ctx, newCapsuleConn(c.reader.Reader, c.conn, destination), source, destination, c.onClose)
return requestHandedOff, nil
}
func (h *httpHandler) serveConnectUDP(ctx context.Context, writer http.ResponseWriter, request *http.Request, source M.Socksaddr) {
destination, valid := parseConnectUDPTarget(request.URL.EscapedPath())
if !valid {
h.server.logger.ErrorContext(ctx, "process connection from ", source, ": invalid connect-udp target: ", request.URL.Path)
writer.WriteHeader(http.StatusBadRequest)
return
}
writer.Header().Set("Capsule-Protocol", "?1")
writer.WriteHeader(http.StatusOK)
writer.(http.Flusher).Flush()
if request.ProtoMajor == 3 && HTTP3StreamFunc != nil {
stream, isDatagramStream := HTTP3StreamFunc(request.Context(), writer)
if isDatagramStream {
localAddr, _ := request.Context().Value(http.LocalAddrContextKey).(net.Addr)
conn := newHTTP3PacketConn(stream, destination, localAddr)
h.handler.NewPacketConnectionEx(ctx, conn, source, destination, nil)
conn.wait(request.Context())
return
}
}
conn := v2rayhttp.NewHTTP2Wrapper(&v2rayhttp.ServerHTTPConn{
HTTP2Conn: v2rayhttp.NewHTTPConn(request.Body, writer),
Flusher: writer.(http.Flusher),
})
done := make(chan struct{})
h.handler.NewPacketConnectionEx(ctx, newCapsuleConn(std_bufio.NewReader(conn), conn, destination), source, destination, N.OnceClose(func(it error) {
close(done)
}))
<-done
conn.CloseWrapper()
}