Files
omv-dijiang/works/patch/protocol/obfhttp/outbound.go.patch
T
2026-04-25 09:04:39 +08:00

427 lines
10 KiB
Diff

--- /dev/null
+++ b/protocol/obfhttp/outbound.go
@@ -0,0 +1,423 @@
+// OMV
+package obfhttp
+
+import (
+ "bytes"
+ "compress/gzip"
+ "context"
+ "encoding/json"
+ "io"
+ "net"
+ "net/http"
+ "sync"
+ "time"
+
+ "github.com/sagernet/sing-box/adapter"
+ "github.com/sagernet/sing-box/adapter/outbound"
+ "github.com/sagernet/sing-box/common/dialer"
+ boxTLS "github.com/sagernet/sing-box/common/tls"
+ C "github.com/sagernet/sing-box/constant"
+ "github.com/sagernet/sing-box/log"
+ "github.com/sagernet/sing-box/option"
+ "github.com/sagernet/sing/common"
+ E "github.com/sagernet/sing/common/exceptions"
+ "github.com/sagernet/sing/common/logger"
+ M "github.com/sagernet/sing/common/metadata"
+ N "github.com/sagernet/sing/common/network"
+ "github.com/sagernet/sing/common/uot"
+)
+
+func RegisterOutbound(registry *outbound.Registry) {
+ outbound.Register[option.ObfHTTPOutboundOptions](registry, C.TypeObfHTTP, NewOutbound)
+}
+
+type Outbound struct {
+ outbound.Adapter
+ ctx context.Context
+ logger logger.ContextLogger
+ dialer N.Dialer
+ serverAddr M.Socksaddr
+ tlsConfig boxTLS.Config
+ client *http.Client
+ encryptor Encryptor
+ codec TextCodec
+ path string
+ username string
+ password string
+ scheme string
+ longPollTimeout time.Duration
+}
+
+func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ObfHTTPOutboundOptions) (adapter.Outbound, error) {
+ outboundDialer, err := dialer.New(ctx, options.DialerOptions, options.ServerIsDomain())
+ if err != nil {
+ return nil, err
+ }
+
+ ob := &Outbound{
+ Adapter: outbound.NewAdapterWithDialerOptions(C.TypeObfHTTP, tag, []string{N.NetworkTCP, N.NetworkUDP}, options.DialerOptions),
+ ctx: ctx,
+ logger: logger,
+ dialer: outboundDialer,
+ serverAddr: options.ServerOptions.Build(),
+ codec: newTextCodec(options.Encoding),
+ path: options.Path,
+ username: options.Username,
+ password: options.Password,
+ scheme: "http",
+ }
+
+ if ob.path == "" {
+ ob.path = "/"
+ }
+ if ob.serverAddr.Port == 0 {
+ ob.serverAddr.Port = 80
+ }
+
+ // Encryption
+ if options.Encryption != nil {
+ enc, err := NewEncryptor(options.Encryption.Method, options.Encryption.Password)
+ if err != nil {
+ return nil, E.Cause(err, "create encryptor")
+ }
+ ob.encryptor = enc
+ } else {
+ ob.encryptor = &noneEncryptor{}
+ }
+
+ ob.longPollTimeout = time.Duration(options.LongPollTimeout)
+ if ob.longPollTimeout == 0 {
+ ob.longPollTimeout = 30 * time.Second
+ }
+
+ // TLS
+ var tlsConfig boxTLS.Config
+ if options.TLS != nil && options.TLS.Enabled {
+ tlsConfig, err = boxTLS.NewClient(ctx, logger, ob.serverAddr.AddrString(), common.PtrValueOrDefault(options.TLS))
+ if err != nil {
+ return nil, E.Cause(err, "create TLS config")
+ }
+ ob.tlsConfig = tlsConfig
+ ob.scheme = "https"
+ }
+
+ transport := &http.Transport{
+ DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
+ conn, err := ob.dialer.DialContext(ctx, N.NetworkTCP, ob.serverAddr)
+ if err != nil {
+ return nil, err
+ }
+ if ob.tlsConfig != nil {
+ tlsConn, err := boxTLS.ClientHandshake(ctx, conn, ob.tlsConfig)
+ if err != nil {
+ conn.Close()
+ return nil, err
+ }
+ return tlsConn, nil
+ }
+ return conn, nil
+ },
+ MaxIdleConns: 100,
+ MaxIdleConnsPerHost: 100,
+ IdleConnTimeout: 90 * time.Second,
+ DisableCompression: true,
+ }
+
+ ob.client = &http.Client{
+ Transport: transport,
+ Timeout: ob.longPollTimeout + 10*time.Second,
+ }
+
+ return ob, nil
+}
+
+func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
+ switch N.NetworkName(network) {
+ case N.NetworkTCP:
+ h.logger.InfoContext(ctx, "outbound connection to ", destination)
+ return h.openConnection(ctx, "tcp", destination)
+ case N.NetworkUDP:
+ h.logger.InfoContext(ctx, "outbound UoT packet connection to ", destination)
+ conn, err := h.openConnection(ctx, "tcp", uot.RequestDestination(uot.Version))
+ if err != nil {
+ return nil, err
+ }
+ return uot.NewLazyConn(conn, uot.Request{
+ IsConnect: true,
+ Destination: destination,
+ }), nil
+ default:
+ return nil, E.New("unsupported network: ", network)
+ }
+}
+
+func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
+ h.logger.InfoContext(ctx, "outbound UoT packet connection to ", destination)
+ conn, err := h.openConnection(ctx, "tcp", uot.RequestDestination(uot.Version))
+ if err != nil {
+ return nil, err
+ }
+ return uot.NewLazyConn(conn, uot.Request{
+ IsConnect: false,
+ Destination: destination,
+ }), nil
+}
+
+func (h *Outbound) openConnection(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
+ // Send open request
+ resp, err := h.doRequest(&request{
+ Action: actionOpen,
+ Destination: destination.String(),
+ Network: network,
+ })
+ if err != nil {
+ return nil, E.Cause(err, "open session")
+ }
+ if !resp.OK {
+ return nil, E.New("open session failed: ", resp.Error)
+ }
+
+ cc := &clientConn{
+ outbound: h,
+ sessionID: resp.Session,
+ ctx: ctx,
+ }
+ cc.cond = sync.NewCond(&cc.mu)
+
+ // Process piggybacked data from open response
+ if resp.Payload != "" {
+ data, err := h.decodePayload(resp.Payload)
+ if err != nil {
+ return nil, E.Cause(err, "decode open response")
+ }
+ cc.readBuf = data
+ }
+
+ // Start background long-poll goroutine
+ go cc.pollLoop()
+
+ return cc, nil
+}
+
+func (h *Outbound) doRequest(req *request) (*response, error) {
+ // Encode payload if present
+ if req.Payload != "" {
+ // Payload is already encoded at this point (by clientConn)
+ }
+
+ // Add padding
+ req.Padding = h.codec.GeneratePadding(randomPaddingLength())
+
+ body, err := json.Marshal(req)
+ if err != nil {
+ return nil, E.Cause(err, "marshal request")
+ }
+
+ url := h.scheme + "://" + h.serverAddr.String() + h.path
+ httpReq, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
+ if err != nil {
+ return nil, E.Cause(err, "create http request")
+ }
+ httpReq.Header.Set("Content-Type", "application/json")
+
+ if h.username != "" || h.password != "" {
+ httpReq.SetBasicAuth(h.username, h.password)
+ }
+
+ httpResp, err := h.client.Do(httpReq)
+ if err != nil {
+ return nil, E.Cause(err, "http request")
+ }
+ defer httpResp.Body.Close()
+
+ respBody, err := h.readResponseBody(httpResp)
+ if err != nil {
+ return nil, E.Cause(err, "read response body")
+ }
+
+ var resp response
+ if err := json.Unmarshal(respBody, &resp); err != nil {
+ return nil, E.Cause(err, "unmarshal response")
+ }
+
+ return &resp, nil
+}
+
+func (h *Outbound) readResponseBody(resp *http.Response) ([]byte, error) {
+ var reader io.Reader = resp.Body
+ if resp.Header.Get("Content-Encoding") == "gzip" {
+ gr, err := gzip.NewReader(resp.Body)
+ if err != nil {
+ return nil, err
+ }
+ defer gr.Close()
+ reader = gr
+ }
+ return io.ReadAll(reader)
+}
+
+func (h *Outbound) encodePayload(data []byte) (string, error) {
+ encrypted, err := h.encryptor.Encrypt(data)
+ if err != nil {
+ return "", err
+ }
+ return h.codec.Encode(encrypted), nil
+}
+
+func (h *Outbound) decodePayload(payload string) ([]byte, error) {
+ decoded, err := h.codec.Decode(payload)
+ if err != nil {
+ return nil, err
+ }
+ return h.encryptor.Decrypt(decoded)
+}
+
+func (h *Outbound) Close() error {
+ h.client.CloseIdleConnections()
+ return common.Close(h.tlsConfig)
+}
+
+// clientConn implements net.Conn, bridging to the HTTP-based obfhttp protocol.
+type clientConn struct {
+ outbound *Outbound
+ sessionID string
+ ctx context.Context
+
+ mu sync.Mutex
+ cond *sync.Cond
+ readBuf []byte
+ closed bool
+ pollDone bool
+}
+
+func (c *clientConn) Read(p []byte) (int, error) {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+
+ for len(c.readBuf) == 0 && !c.closed {
+ c.cond.Wait()
+ }
+ if len(c.readBuf) == 0 && c.closed {
+ return 0, net.ErrClosed
+ }
+
+ n := copy(p, c.readBuf)
+ c.readBuf = c.readBuf[n:]
+ return n, nil
+}
+
+func (c *clientConn) Write(p []byte) (int, error) {
+ if c.closed {
+ return 0, net.ErrClosed
+ }
+
+ encoded, err := c.outbound.encodePayload(p)
+ if err != nil {
+ return 0, err
+ }
+
+ resp, err := c.outbound.doRequest(&request{
+ Session: c.sessionID,
+ Action: actionData,
+ Payload: encoded,
+ })
+ if err != nil {
+ return 0, err
+ }
+ if !resp.OK {
+ return 0, E.New("data request failed: ", resp.Error)
+ }
+
+ // Process piggybacked downstream data
+ if resp.Payload != "" {
+ data, err := c.outbound.decodePayload(resp.Payload)
+ if err != nil {
+ return 0, E.Cause(err, "decode piggyback data")
+ }
+ c.mu.Lock()
+ c.readBuf = append(c.readBuf, data...)
+ c.cond.Broadcast()
+ c.mu.Unlock()
+ }
+
+ return len(p), nil
+}
+
+func (c *clientConn) Close() error {
+ c.mu.Lock()
+ if c.closed {
+ c.mu.Unlock()
+ return nil
+ }
+ c.closed = true
+ c.cond.Broadcast()
+ c.mu.Unlock()
+
+ // Send close to server (best effort)
+ c.outbound.doRequest(&request{
+ Session: c.sessionID,
+ Action: actionClose,
+ })
+ return nil
+}
+
+func (c *clientConn) pollLoop() {
+ defer func() {
+ c.mu.Lock()
+ c.pollDone = true
+ c.cond.Broadcast()
+ c.mu.Unlock()
+ }()
+
+ for {
+ c.mu.Lock()
+ closed := c.closed
+ c.mu.Unlock()
+ if closed {
+ return
+ }
+
+ resp, err := c.outbound.doRequest(&request{
+ Session: c.sessionID,
+ Action: actionRecv,
+ })
+ if err != nil {
+ c.mu.Lock()
+ c.closed = true
+ c.cond.Broadcast()
+ c.mu.Unlock()
+ return
+ }
+ if !resp.OK {
+ c.mu.Lock()
+ c.closed = true
+ c.cond.Broadcast()
+ c.mu.Unlock()
+ return
+ }
+
+ if resp.Payload != "" {
+ data, err := c.outbound.decodePayload(resp.Payload)
+ if err != nil {
+ c.mu.Lock()
+ c.closed = true
+ c.cond.Broadcast()
+ c.mu.Unlock()
+ return
+ }
+ c.mu.Lock()
+ c.readBuf = append(c.readBuf, data...)
+ c.cond.Broadcast()
+ c.mu.Unlock()
+ }
+ }
+}
+
+func (c *clientConn) LocalAddr() net.Addr { return &simpleAddr{network: "tcp", address: "obfhttp-client"} }
+func (c *clientConn) RemoteAddr() net.Addr { return &simpleAddr{network: "tcp", address: c.outbound.serverAddr.String()} }
+func (c *clientConn) SetDeadline(t time.Time) error { return nil }
+func (c *clientConn) SetReadDeadline(t time.Time) error { return nil }
+func (c *clientConn) SetWriteDeadline(t time.Time) error { return nil }
+
+// compile-time check
+var _ net.Conn = (*clientConn)(nil)