feature: MITM
This commit is contained in:
@@ -36,7 +36,7 @@ func HandleConn(c net.Conn, in chan<- C.ConnContext, cache *cache.LruCache[strin
|
||||
var resp *http.Response
|
||||
|
||||
if !trusted {
|
||||
resp = authenticate(request, cache)
|
||||
resp = Authenticate(request, cache)
|
||||
|
||||
trusted = resp == nil
|
||||
}
|
||||
@@ -66,19 +66,19 @@ func HandleConn(c net.Conn, in chan<- C.ConnContext, cache *cache.LruCache[strin
|
||||
return // hijack connection
|
||||
}
|
||||
|
||||
removeHopByHopHeaders(request.Header)
|
||||
removeExtraHTTPHostPort(request)
|
||||
RemoveHopByHopHeaders(request.Header)
|
||||
RemoveExtraHTTPHostPort(request)
|
||||
|
||||
if request.URL.Scheme == "" || request.URL.Host == "" {
|
||||
resp = responseWith(request, http.StatusBadRequest)
|
||||
resp = ResponseWith(request, http.StatusBadRequest)
|
||||
} else {
|
||||
resp, err = client.Do(request)
|
||||
if err != nil {
|
||||
resp = responseWith(request, http.StatusBadGateway)
|
||||
resp = ResponseWith(request, http.StatusBadGateway)
|
||||
}
|
||||
}
|
||||
|
||||
removeHopByHopHeaders(resp.Header)
|
||||
RemoveHopByHopHeaders(resp.Header)
|
||||
}
|
||||
|
||||
if keepAlive {
|
||||
@@ -98,12 +98,12 @@ func HandleConn(c net.Conn, in chan<- C.ConnContext, cache *cache.LruCache[strin
|
||||
_ = conn.Close()
|
||||
}
|
||||
|
||||
func authenticate(request *http.Request, cache *cache.LruCache[string, bool]) *http.Response {
|
||||
func Authenticate(request *http.Request, cache *cache.LruCache[string, bool]) *http.Response {
|
||||
authenticator := authStore.Authenticator()
|
||||
if authenticator != nil {
|
||||
credential := parseBasicProxyAuthorization(request)
|
||||
if credential == "" {
|
||||
resp := responseWith(request, http.StatusProxyAuthRequired)
|
||||
resp := ResponseWith(request, http.StatusProxyAuthRequired)
|
||||
resp.Header.Set("Proxy-Authenticate", "Basic")
|
||||
return resp
|
||||
}
|
||||
@@ -117,14 +117,14 @@ func authenticate(request *http.Request, cache *cache.LruCache[string, bool]) *h
|
||||
if !authed {
|
||||
log.Infoln("Auth failed from %s", request.RemoteAddr)
|
||||
|
||||
return responseWith(request, http.StatusForbidden)
|
||||
return ResponseWith(request, http.StatusForbidden)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func responseWith(request *http.Request, statusCode int) *http.Response {
|
||||
func ResponseWith(request *http.Request, statusCode int) *http.Response {
|
||||
return &http.Response{
|
||||
StatusCode: statusCode,
|
||||
Status: http.StatusText(statusCode),
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Dreamacro/clash/adapter/inbound"
|
||||
N "github.com/Dreamacro/clash/common/net"
|
||||
@@ -29,7 +30,7 @@ func handleUpgrade(conn net.Conn, request *http.Request, in chan<- C.ConnContext
|
||||
defer conn.Close()
|
||||
|
||||
removeProxyHeaders(request.Header)
|
||||
removeExtraHTTPHostPort(request)
|
||||
RemoveExtraHTTPHostPort(request)
|
||||
|
||||
address := request.Host
|
||||
if _, _, err := net.SplitHostPort(address); err != nil {
|
||||
@@ -87,3 +88,65 @@ func handleUpgrade(conn net.Conn, request *http.Request, in chan<- C.ConnContext
|
||||
N.Relay(bufferedLeft, conn)
|
||||
}
|
||||
}
|
||||
|
||||
func HandleUpgradeY(localConn net.Conn, serverConn *N.BufferedConn, request *http.Request, in chan<- C.ConnContext) (resp *http.Response) {
|
||||
removeProxyHeaders(request.Header)
|
||||
RemoveExtraHTTPHostPort(request)
|
||||
|
||||
if serverConn == nil {
|
||||
address := request.Host
|
||||
if _, _, err := net.SplitHostPort(address); err != nil {
|
||||
port := "80"
|
||||
if request.TLS != nil {
|
||||
port = "443"
|
||||
}
|
||||
address = net.JoinHostPort(address, port)
|
||||
}
|
||||
|
||||
dstAddr := socks5.ParseAddr(address)
|
||||
if dstAddr == nil {
|
||||
return
|
||||
}
|
||||
|
||||
left, right := net.Pipe()
|
||||
|
||||
in <- inbound.NewHTTP(dstAddr, localConn.RemoteAddr(), right)
|
||||
|
||||
serverConn = N.NewBufferedConn(left)
|
||||
|
||||
defer func() {
|
||||
_ = serverConn.Close()
|
||||
}()
|
||||
}
|
||||
|
||||
err := request.Write(serverConn)
|
||||
if err != nil {
|
||||
_ = localConn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
resp, err = http.ReadResponse(serverConn.Reader(), request)
|
||||
if err != nil {
|
||||
_ = localConn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusSwitchingProtocols {
|
||||
removeProxyHeaders(resp.Header)
|
||||
|
||||
err = localConn.SetReadDeadline(time.Time{}) // set to not time out
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = resp.Write(localConn)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
N.Relay(serverConn, localConn) // blocking here
|
||||
_ = localConn.Close()
|
||||
resp = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ func removeProxyHeaders(header http.Header) {
|
||||
header.Del("Proxy-Authorization")
|
||||
}
|
||||
|
||||
// removeHopByHopHeaders remove hop-by-hop header
|
||||
func removeHopByHopHeaders(header http.Header) {
|
||||
// RemoveHopByHopHeaders remove hop-by-hop header
|
||||
func RemoveHopByHopHeaders(header http.Header) {
|
||||
// Strip hop-by-hop header based on RFC:
|
||||
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.1
|
||||
// https://www.mnot.net/blog/2011/07/11/what_proxies_must_do
|
||||
@@ -38,9 +38,9 @@ func removeHopByHopHeaders(header http.Header) {
|
||||
}
|
||||
}
|
||||
|
||||
// removeExtraHTTPHostPort remove extra host port (example.com:80 --> example.com)
|
||||
// RemoveExtraHTTPHostPort remove extra host port (example.com:80 --> example.com)
|
||||
// It resolves the behavior of some HTTP servers that do not handle host:80 (e.g. baidu.com)
|
||||
func removeExtraHTTPHostPort(req *http.Request) {
|
||||
func RemoveExtraHTTPHostPort(req *http.Request) {
|
||||
host := req.Host
|
||||
if host == "" {
|
||||
host = req.URL.Host
|
||||
|
||||
@@ -1,19 +1,26 @@
|
||||
package listener
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"golang.org/x/exp/slices"
|
||||
"net"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Dreamacro/clash/common/cert"
|
||||
"github.com/Dreamacro/clash/component/ebpf"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/listener/autoredir"
|
||||
LC "github.com/Dreamacro/clash/listener/config"
|
||||
"github.com/Dreamacro/clash/listener/http"
|
||||
"github.com/Dreamacro/clash/listener/mitm"
|
||||
"github.com/Dreamacro/clash/listener/mixed"
|
||||
"github.com/Dreamacro/clash/listener/redir"
|
||||
embedSS "github.com/Dreamacro/clash/listener/shadowsocks"
|
||||
@@ -23,9 +30,10 @@ import (
|
||||
"github.com/Dreamacro/clash/listener/socks"
|
||||
"github.com/Dreamacro/clash/listener/tproxy"
|
||||
"github.com/Dreamacro/clash/listener/tuic"
|
||||
"github.com/Dreamacro/clash/listener/tunnel"
|
||||
LT "github.com/Dreamacro/clash/listener/tunnel"
|
||||
"github.com/Dreamacro/clash/log"
|
||||
|
||||
rewrites "github.com/Dreamacro/clash/rewrite"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
@@ -42,8 +50,8 @@ var (
|
||||
tproxyUDPListener *tproxy.UDPListener
|
||||
mixedListener *mixed.Listener
|
||||
mixedUDPLister *socks.UDPListener
|
||||
tunnelTCPListeners = map[string]*tunnel.Listener{}
|
||||
tunnelUDPListeners = map[string]*tunnel.PacketConn{}
|
||||
tunnelTCPListeners = map[string]*LT.Listener{}
|
||||
tunnelUDPListeners = map[string]*LT.PacketConn{}
|
||||
inboundListeners = map[string]C.InboundListener{}
|
||||
tunLister *sing_tun.Listener
|
||||
shadowSocksListener C.MultiAddrListener
|
||||
@@ -52,6 +60,7 @@ var (
|
||||
autoRedirListener *autoredir.Listener
|
||||
autoRedirProgram *ebpf.TcEBpfProgram
|
||||
tcProgram *ebpf.TcEBpfProgram
|
||||
mitmListener *mitm.Listener
|
||||
|
||||
// lock for recreate function
|
||||
socksMux sync.Mutex
|
||||
@@ -67,6 +76,7 @@ var (
|
||||
tuicMux sync.Mutex
|
||||
autoRedirMux sync.Mutex
|
||||
tcMux sync.Mutex
|
||||
mitmMux sync.Mutex
|
||||
|
||||
LastTunConf LC.Tun
|
||||
LastTuicConf LC.TuicServer
|
||||
@@ -80,6 +90,7 @@ type Ports struct {
|
||||
MixedPort int `json:"mixed-port"`
|
||||
ShadowSocksConfig string `json:"ss-config"`
|
||||
VmessConfig string `json:"vmess-config"`
|
||||
MitmPort int `json:"mitm-port"`
|
||||
}
|
||||
|
||||
func GetTunConf() LC.Tun {
|
||||
@@ -699,7 +710,7 @@ func PatchTunnel(tunnels []LC.Tunnel, tcpIn chan<- C.ConnContext, udpIn chan<- C
|
||||
for _, elm := range needCreate {
|
||||
key := fmt.Sprintf("%s/%s/%s", elm.addr, elm.target, elm.proxy)
|
||||
if elm.network == "tcp" {
|
||||
l, err := tunnel.New(elm.addr, elm.target, elm.proxy, tcpIn)
|
||||
l, err := LT.New(elm.addr, elm.target, elm.proxy, tcpIn)
|
||||
if err != nil {
|
||||
log.Errorln("Start tunnel %s error: %s", elm.target, err.Error())
|
||||
continue
|
||||
@@ -707,7 +718,7 @@ func PatchTunnel(tunnels []LC.Tunnel, tcpIn chan<- C.ConnContext, udpIn chan<- C
|
||||
tunnelTCPListeners[key] = l
|
||||
log.Infoln("Tunnel(tcp/%s) proxy %s listening at: %s", elm.target, elm.proxy, tunnelTCPListeners[key].Address())
|
||||
} else {
|
||||
l, err := tunnel.NewUDP(elm.addr, elm.target, elm.proxy, udpIn)
|
||||
l, err := LT.NewUDP(elm.addr, elm.target, elm.proxy, udpIn)
|
||||
if err != nil {
|
||||
log.Errorln("Start tunnel %s error: %s", elm.target, err.Error())
|
||||
continue
|
||||
@@ -747,6 +758,79 @@ func PatchInboundListeners(newListenerMap map[string]C.InboundListener, tcpIn ch
|
||||
}
|
||||
}
|
||||
|
||||
func ReCreateMitm(port int, tcpIn chan<- C.ConnContext) {
|
||||
mitmMux.Lock()
|
||||
defer mitmMux.Unlock()
|
||||
|
||||
var err error
|
||||
defer func() {
|
||||
if err != nil {
|
||||
log.Errorln("Start MITM server error: %s", err.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
addr := genAddr(bindAddress, port, allowLan)
|
||||
|
||||
if mitmListener != nil {
|
||||
if mitmListener.RawAddress() == addr {
|
||||
return
|
||||
}
|
||||
_ = mitmListener.Close()
|
||||
mitmListener = nil
|
||||
}
|
||||
|
||||
if portIsZero(addr) {
|
||||
return
|
||||
}
|
||||
|
||||
if err = initCert(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
rootCACert tls.Certificate
|
||||
x509c *x509.Certificate
|
||||
certOption *cert.Config
|
||||
)
|
||||
|
||||
rootCACert, err = tls.LoadX509KeyPair(C.Path.RootCA(), C.Path.CAKey())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
privateKey := rootCACert.PrivateKey.(*rsa.PrivateKey)
|
||||
|
||||
x509c, err = x509.ParseCertificate(rootCACert.Certificate[0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
certOption, err = cert.NewConfig(
|
||||
x509c,
|
||||
privateKey,
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
certOption.SetValidity(time.Hour * 24 * 365 * 2) // 2 years
|
||||
certOption.SetOrganization("Clash ManInTheMiddle Proxy Services")
|
||||
|
||||
opt := &mitm.Option{
|
||||
Addr: addr,
|
||||
ApiHost: "mitm.clash",
|
||||
CertConfig: certOption,
|
||||
Handler: &rewrites.RewriteHandler{},
|
||||
}
|
||||
|
||||
mitmListener, err = mitm.New(opt, tcpIn)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
log.Infoln("Mitm proxy listening at: %s", mitmListener.Address())
|
||||
}
|
||||
|
||||
// GetPorts return the ports of proxy servers
|
||||
func GetPorts() *Ports {
|
||||
ports := &Ports{}
|
||||
@@ -789,6 +873,12 @@ func GetPorts() *Ports {
|
||||
ports.VmessConfig = vmessListener.Config()
|
||||
}
|
||||
|
||||
if mitmListener != nil {
|
||||
_, portStr, _ := net.SplitHostPort(mitmListener.Address())
|
||||
port, _ := strconv.Atoi(portStr)
|
||||
ports.MitmPort = port
|
||||
}
|
||||
|
||||
return ports
|
||||
}
|
||||
|
||||
@@ -902,6 +992,19 @@ func closeTunListener() {
|
||||
}
|
||||
}
|
||||
|
||||
func initCert() error {
|
||||
if _, err := os.Stat(C.Path.RootCA()); os.IsNotExist(err) {
|
||||
log.Infoln("Can't find mitm_ca.crt, start generate")
|
||||
err = cert.GenerateAndSave(C.Path.RootCA(), C.Path.CAKey())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infoln("Generated CA private key and CA certificate finish")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Cleanup() {
|
||||
closeTunListener()
|
||||
}
|
||||
|
||||
55
listener/mitm/client.go
Normal file
55
listener/mitm/client.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package mitm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/Dreamacro/clash/adapter/inbound"
|
||||
N "github.com/Dreamacro/clash/common/net"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/transport/socks5"
|
||||
)
|
||||
|
||||
func getServerConn(serverConn *N.BufferedConn, request *http.Request, srcAddr net.Addr, in chan<- C.ConnContext) (*N.BufferedConn, error) {
|
||||
if serverConn != nil {
|
||||
return serverConn, nil
|
||||
}
|
||||
|
||||
address := request.URL.Host
|
||||
if _, _, err := net.SplitHostPort(address); err != nil {
|
||||
port := "80"
|
||||
if request.TLS != nil {
|
||||
port = "443"
|
||||
}
|
||||
address = net.JoinHostPort(address, port)
|
||||
}
|
||||
|
||||
dstAddr := socks5.ParseAddr(address)
|
||||
if dstAddr == nil {
|
||||
return nil, socks5.ErrAddressNotSupported
|
||||
}
|
||||
|
||||
left, right := net.Pipe()
|
||||
|
||||
in <- inbound.NewMitm(dstAddr, srcAddr, request.Header.Get("User-Agent"), right)
|
||||
|
||||
if request.TLS != nil {
|
||||
tlsConn := tls.Client(left, &tls.Config{
|
||||
ServerName: request.TLS.ServerName,
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout)
|
||||
defer cancel()
|
||||
if err := tlsConn.HandshakeContext(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
serverConn = N.NewBufferedConn(tlsConn)
|
||||
} else {
|
||||
serverConn = N.NewBufferedConn(left)
|
||||
}
|
||||
|
||||
return serverConn, nil
|
||||
}
|
||||
9
listener/mitm/hack.go
Normal file
9
listener/mitm/hack.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package mitm
|
||||
|
||||
import (
|
||||
_ "net/http"
|
||||
_ "unsafe"
|
||||
)
|
||||
|
||||
//go:linkname validMethod net/http.validMethod
|
||||
func validMethod(method string) bool
|
||||
349
listener/mitm/proxy.go
Normal file
349
listener/mitm/proxy.go
Normal file
@@ -0,0 +1,349 @@
|
||||
package mitm
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Dreamacro/clash/common/cache"
|
||||
N "github.com/Dreamacro/clash/common/net"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
H "github.com/Dreamacro/clash/listener/http"
|
||||
)
|
||||
|
||||
func HandleConn(c net.Conn, opt *Option, in chan<- C.ConnContext, cache *cache.LruCache[string, bool]) {
|
||||
var (
|
||||
clientIP = netip.MustParseAddrPort(c.RemoteAddr().String()).Addr()
|
||||
sourceAddr net.Addr
|
||||
serverConn *N.BufferedConn
|
||||
connState *tls.ConnectionState
|
||||
)
|
||||
|
||||
defer func() {
|
||||
if serverConn != nil {
|
||||
_ = serverConn.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
conn := N.NewBufferedConn(c)
|
||||
|
||||
trusted := cache == nil // disable authenticate if cache is nil
|
||||
if !trusted {
|
||||
trusted = clientIP.IsLoopback() || clientIP.IsUnspecified()
|
||||
}
|
||||
|
||||
readLoop:
|
||||
for {
|
||||
// use SetReadDeadline instead of Proxy-Connection keep-alive
|
||||
if err := conn.SetReadDeadline(time.Now().Add(65 * time.Second)); err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
request, err := H.ReadRequest(conn.Reader())
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
var response *http.Response
|
||||
|
||||
session := newSession(conn, request, response)
|
||||
|
||||
sourceAddr = parseSourceAddress(session.request, conn.RemoteAddr(), sourceAddr)
|
||||
session.request.RemoteAddr = sourceAddr.String()
|
||||
|
||||
if !trusted {
|
||||
session.response = H.Authenticate(session.request, cache)
|
||||
|
||||
trusted = session.response == nil
|
||||
}
|
||||
|
||||
if trusted {
|
||||
if session.request.Method == http.MethodConnect {
|
||||
if session.request.ProtoMajor > 1 {
|
||||
session.request.ProtoMajor = 1
|
||||
session.request.ProtoMinor = 1
|
||||
}
|
||||
|
||||
// Manual writing to support CONNECT for http 1.0 (workaround for uplay client)
|
||||
if _, err = fmt.Fprintf(session.conn, "HTTP/%d.%d %03d %s\r\n\r\n", session.request.ProtoMajor, session.request.ProtoMinor, http.StatusOK, "Connection established"); err != nil {
|
||||
handleError(opt, session, err)
|
||||
break // close connection
|
||||
}
|
||||
|
||||
if strings.HasSuffix(session.request.URL.Host, ":80") {
|
||||
goto readLoop
|
||||
}
|
||||
|
||||
b, err1 := conn.Peek(1)
|
||||
if err1 != nil {
|
||||
handleError(opt, session, err1)
|
||||
break // close connection
|
||||
}
|
||||
|
||||
// TLS handshake.
|
||||
if b[0] == 0x16 {
|
||||
tlsConn := tls.Server(conn, opt.CertConfig.NewTLSConfigForHost(session.request.URL.Hostname()))
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout)
|
||||
// handshake with the local client
|
||||
if err = tlsConn.HandshakeContext(ctx); err != nil {
|
||||
cancel()
|
||||
session.response = session.NewErrorResponse(fmt.Errorf("handshake failed: %w", err))
|
||||
_ = writeResponse(session, false)
|
||||
break // close connection
|
||||
}
|
||||
cancel()
|
||||
|
||||
cs := tlsConn.ConnectionState()
|
||||
connState = &cs
|
||||
|
||||
conn = N.NewBufferedConn(tlsConn)
|
||||
}
|
||||
|
||||
if strings.HasSuffix(session.request.URL.Host, ":443") {
|
||||
goto readLoop
|
||||
}
|
||||
|
||||
if conn.SetReadDeadline(time.Now().Add(time.Second)) != nil {
|
||||
break
|
||||
}
|
||||
|
||||
buf, err2 := conn.Peek(7)
|
||||
if err2 != nil {
|
||||
if err2 != bufio.ErrBufferFull && !os.IsTimeout(err2) {
|
||||
handleError(opt, session, err2)
|
||||
break // close connection
|
||||
}
|
||||
}
|
||||
|
||||
// others protocol over tcp
|
||||
if !isHTTPTraffic(buf) {
|
||||
if connState != nil {
|
||||
session.request.TLS = connState
|
||||
}
|
||||
|
||||
serverConn, err = getServerConn(serverConn, session.request, sourceAddr, in)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
if conn.SetReadDeadline(time.Time{}) != nil {
|
||||
break
|
||||
}
|
||||
|
||||
N.Relay(serverConn, conn)
|
||||
return // hijack connection
|
||||
}
|
||||
|
||||
goto readLoop
|
||||
}
|
||||
|
||||
prepareRequest(connState, session.request)
|
||||
|
||||
// hijack api
|
||||
if session.request.URL.Hostname() == opt.ApiHost {
|
||||
if err = handleApiRequest(session, opt); err != nil {
|
||||
handleError(opt, session, err)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// forward websocket
|
||||
if isWebsocketRequest(request) {
|
||||
serverConn, err = getServerConn(serverConn, session.request, sourceAddr, in)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
session.request.RequestURI = ""
|
||||
if session.response = H.HandleUpgradeY(conn, serverConn, request, in); session.response == nil {
|
||||
return // hijack connection
|
||||
}
|
||||
}
|
||||
|
||||
if session.response == nil {
|
||||
H.RemoveHopByHopHeaders(session.request.Header)
|
||||
H.RemoveExtraHTTPHostPort(session.request)
|
||||
|
||||
// hijack custom request and write back custom response if necessary
|
||||
newReq, newRes := opt.Handler.HandleRequest(session)
|
||||
if newReq != nil {
|
||||
session.request = newReq
|
||||
}
|
||||
if newRes != nil {
|
||||
session.response = newRes
|
||||
|
||||
if err = writeResponse(session, false); err != nil {
|
||||
handleError(opt, session, err)
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
session.request.RequestURI = ""
|
||||
|
||||
if session.request.URL.Host == "" {
|
||||
session.response = session.NewErrorResponse(ErrInvalidURL)
|
||||
} else {
|
||||
serverConn, err = getServerConn(serverConn, session.request, sourceAddr, in)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
// send the request to remote server
|
||||
err = session.request.Write(serverConn)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
session.response, err = http.ReadResponse(serverConn.Reader(), request)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err = writeResponseWithHandler(session, opt); err != nil {
|
||||
handleError(opt, session, err)
|
||||
break // close connection
|
||||
}
|
||||
}
|
||||
|
||||
_ = conn.Close()
|
||||
}
|
||||
|
||||
func writeResponseWithHandler(session *Session, opt *Option) error {
|
||||
res := opt.Handler.HandleResponse(session)
|
||||
if res != nil {
|
||||
session.response = res
|
||||
}
|
||||
|
||||
return writeResponse(session, true)
|
||||
}
|
||||
|
||||
func writeResponse(session *Session, keepAlive bool) error {
|
||||
H.RemoveHopByHopHeaders(session.response.Header)
|
||||
|
||||
if keepAlive {
|
||||
session.response.Header.Set("Connection", "keep-alive")
|
||||
session.response.Header.Set("Keep-Alive", "timeout=60")
|
||||
}
|
||||
|
||||
return session.writeResponse()
|
||||
}
|
||||
|
||||
func handleApiRequest(session *Session, opt *Option) error {
|
||||
if opt.CertConfig != nil && strings.ToLower(session.request.URL.Path) == "/cert.crt" {
|
||||
b := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: opt.CertConfig.GetCA().Raw,
|
||||
})
|
||||
|
||||
session.response = session.NewResponse(http.StatusOK, bytes.NewReader(b))
|
||||
|
||||
session.response.Close = true
|
||||
session.response.Header.Set("Content-Type", "application/x-x509-ca-cert")
|
||||
session.response.ContentLength = int64(len(b))
|
||||
|
||||
return session.writeResponse()
|
||||
}
|
||||
|
||||
b := `<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
|
||||
<html><head>
|
||||
<title>Clash MITM Proxy Services - 404 Not Found</title>
|
||||
</head><body>
|
||||
<h1>Not Found</h1>
|
||||
<p>The requested URL %s was not found on this server.</p>
|
||||
</body></html>
|
||||
`
|
||||
|
||||
if opt.Handler.HandleApiRequest(session) {
|
||||
return nil
|
||||
}
|
||||
|
||||
b = fmt.Sprintf(b, session.request.URL.Path)
|
||||
|
||||
session.response = session.NewResponse(http.StatusNotFound, bytes.NewReader([]byte(b)))
|
||||
session.response.Close = true
|
||||
session.response.Header.Set("Content-Type", "text/html;charset=utf-8")
|
||||
session.response.ContentLength = int64(len(b))
|
||||
|
||||
return session.writeResponse()
|
||||
}
|
||||
|
||||
func handleError(opt *Option, session *Session, err error) {
|
||||
if session.response != nil {
|
||||
defer func() {
|
||||
_, _ = io.Copy(io.Discard, session.response.Body)
|
||||
_ = session.response.Body.Close()
|
||||
}()
|
||||
}
|
||||
opt.Handler.HandleError(session, err)
|
||||
}
|
||||
|
||||
func prepareRequest(connState *tls.ConnectionState, request *http.Request) {
|
||||
host := request.Header.Get("Host")
|
||||
if host != "" {
|
||||
request.Host = host
|
||||
}
|
||||
|
||||
if request.URL.Host == "" {
|
||||
request.URL.Host = request.Host
|
||||
}
|
||||
|
||||
if request.URL.Scheme == "" {
|
||||
request.URL.Scheme = "http"
|
||||
}
|
||||
|
||||
if connState != nil {
|
||||
request.TLS = connState
|
||||
request.URL.Scheme = "https"
|
||||
}
|
||||
|
||||
if request.Header.Get("Accept-Encoding") != "" {
|
||||
request.Header.Set("Accept-Encoding", "gzip")
|
||||
}
|
||||
}
|
||||
|
||||
func parseSourceAddress(req *http.Request, connSource, source net.Addr) net.Addr {
|
||||
if source != nil {
|
||||
return source
|
||||
}
|
||||
|
||||
sourceAddress := req.Header.Get("Origin-Request-Source-Address")
|
||||
if sourceAddress == "" {
|
||||
return connSource
|
||||
}
|
||||
|
||||
req.Header.Del("Origin-Request-Source-Address")
|
||||
|
||||
addrPort, err := netip.ParseAddrPort(sourceAddress)
|
||||
if err != nil {
|
||||
return connSource
|
||||
}
|
||||
|
||||
return &net.TCPAddr{
|
||||
IP: addrPort.Addr().AsSlice(),
|
||||
Port: int(addrPort.Port()),
|
||||
}
|
||||
}
|
||||
|
||||
func isWebsocketRequest(req *http.Request) bool {
|
||||
return strings.EqualFold(req.Header.Get("Connection"), "Upgrade") && strings.EqualFold(req.Header.Get("Upgrade"), "websocket")
|
||||
}
|
||||
|
||||
func isHTTPTraffic(buf []byte) bool {
|
||||
method, _, _ := strings.Cut(string(buf), " ")
|
||||
return validMethod(method)
|
||||
}
|
||||
88
listener/mitm/server.go
Normal file
88
listener/mitm/server.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package mitm
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"github.com/Dreamacro/clash/common/cache"
|
||||
"github.com/Dreamacro/clash/common/cert"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"net"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Handler interface {
|
||||
HandleRequest(*Session) (*http.Request, *http.Response) // Session.Response maybe nil
|
||||
HandleResponse(*Session) *http.Response
|
||||
HandleApiRequest(*Session) bool
|
||||
HandleError(*Session, error) // Session maybe nil
|
||||
}
|
||||
|
||||
type Option struct {
|
||||
Addr string
|
||||
ApiHost string
|
||||
|
||||
TLSConfig *tls.Config
|
||||
CertConfig *cert.Config
|
||||
|
||||
Handler Handler
|
||||
}
|
||||
|
||||
type Listener struct {
|
||||
*Option
|
||||
|
||||
listener net.Listener
|
||||
addr string
|
||||
closed bool
|
||||
}
|
||||
|
||||
// RawAddress implements C.Listener
|
||||
func (l *Listener) RawAddress() string {
|
||||
return l.addr
|
||||
}
|
||||
|
||||
// Address implements C.Listener
|
||||
func (l *Listener) Address() string {
|
||||
return l.listener.Addr().String()
|
||||
}
|
||||
|
||||
// Close implements C.Listener
|
||||
func (l *Listener) Close() error {
|
||||
l.closed = true
|
||||
return l.listener.Close()
|
||||
}
|
||||
|
||||
// New the MITM proxy actually is a type of HTTP proxy
|
||||
func New(option *Option, in chan<- C.ConnContext) (*Listener, error) {
|
||||
return NewWithAuthenticate(option, in, true)
|
||||
}
|
||||
|
||||
func NewWithAuthenticate(option *Option, in chan<- C.ConnContext, authenticate bool) (*Listener, error) {
|
||||
l, err := net.Listen("tcp", option.Addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var c *cache.LruCache[string, bool]
|
||||
if authenticate {
|
||||
c = cache.New[string, bool](cache.WithAge[string, bool](90))
|
||||
}
|
||||
|
||||
hl := &Listener{
|
||||
listener: l,
|
||||
addr: option.Addr,
|
||||
Option: option,
|
||||
}
|
||||
go func() {
|
||||
for {
|
||||
conn, err1 := hl.listener.Accept()
|
||||
if err1 != nil {
|
||||
if hl.closed {
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
go HandleConn(conn, option, in, c)
|
||||
}
|
||||
}()
|
||||
|
||||
return hl, nil
|
||||
}
|
||||
59
listener/mitm/session.go
Normal file
59
listener/mitm/session.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package mitm
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Session struct {
|
||||
conn net.Conn
|
||||
request *http.Request
|
||||
response *http.Response
|
||||
|
||||
props map[string]any
|
||||
}
|
||||
|
||||
func (s *Session) Request() *http.Request {
|
||||
return s.request
|
||||
}
|
||||
|
||||
func (s *Session) Response() *http.Response {
|
||||
return s.response
|
||||
}
|
||||
|
||||
func (s *Session) GetProperties(key string) (any, bool) {
|
||||
v, ok := s.props[key]
|
||||
return v, ok
|
||||
}
|
||||
|
||||
func (s *Session) SetProperties(key string, val any) {
|
||||
s.props[key] = val
|
||||
}
|
||||
|
||||
func (s *Session) NewResponse(code int, body io.Reader) *http.Response {
|
||||
return NewResponse(code, body, s.request)
|
||||
}
|
||||
|
||||
func (s *Session) NewErrorResponse(err error) *http.Response {
|
||||
return NewErrorResponse(s.request, err)
|
||||
}
|
||||
|
||||
func (s *Session) writeResponse() error {
|
||||
if s.response == nil {
|
||||
return ErrInvalidResponse
|
||||
}
|
||||
defer func(resp *http.Response) {
|
||||
_ = resp.Body.Close()
|
||||
}(s.response)
|
||||
return s.response.Write(s.conn)
|
||||
}
|
||||
|
||||
func newSession(conn net.Conn, request *http.Request, response *http.Response) *Session {
|
||||
return &Session{
|
||||
conn: conn,
|
||||
request: request,
|
||||
response: response,
|
||||
props: map[string]any{},
|
||||
}
|
||||
}
|
||||
95
listener/mitm/utils.go
Normal file
95
listener/mitm/utils.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package mitm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"golang.org/x/text/encoding/charmap"
|
||||
"golang.org/x/text/transform"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidResponse = errors.New("invalid response")
|
||||
ErrInvalidURL = errors.New("invalid URL")
|
||||
)
|
||||
|
||||
func NewResponse(code int, body io.Reader, req *http.Request) *http.Response {
|
||||
if body == nil {
|
||||
body = &bytes.Buffer{}
|
||||
}
|
||||
|
||||
rc, ok := body.(io.ReadCloser)
|
||||
if !ok {
|
||||
rc = ioutil.NopCloser(body)
|
||||
}
|
||||
|
||||
res := &http.Response{
|
||||
StatusCode: code,
|
||||
Status: fmt.Sprintf("%d %s", code, http.StatusText(code)),
|
||||
Proto: "HTTP/1.1",
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 1,
|
||||
Header: http.Header{},
|
||||
Body: rc,
|
||||
Request: req,
|
||||
}
|
||||
|
||||
if req != nil {
|
||||
res.Close = req.Close
|
||||
res.Proto = req.Proto
|
||||
res.ProtoMajor = req.ProtoMajor
|
||||
res.ProtoMinor = req.ProtoMinor
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func NewErrorResponse(req *http.Request, err error) *http.Response {
|
||||
res := NewResponse(http.StatusBadGateway, nil, req)
|
||||
res.Close = true
|
||||
|
||||
date := res.Header.Get("Date")
|
||||
if date == "" {
|
||||
date = time.Now().Format(http.TimeFormat)
|
||||
}
|
||||
|
||||
w := fmt.Sprintf(`199 "clash" %q %q`, err.Error(), date)
|
||||
res.Header.Add("Warning", w)
|
||||
return res
|
||||
}
|
||||
|
||||
func ReadDecompressedBody(res *http.Response) ([]byte, error) {
|
||||
rBody := res.Body
|
||||
if res.Header.Get("Content-Encoding") == "gzip" {
|
||||
gzReader, err := gzip.NewReader(rBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rBody = gzReader
|
||||
|
||||
defer func(gzReader *gzip.Reader) {
|
||||
_ = gzReader.Close()
|
||||
}(gzReader)
|
||||
}
|
||||
return ioutil.ReadAll(rBody)
|
||||
}
|
||||
|
||||
func DecodeLatin1(reader io.Reader) (string, error) {
|
||||
r := transform.NewReader(reader, charmap.ISO8859_1.NewDecoder())
|
||||
b, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
func EncodeLatin1(str string) ([]byte, error) {
|
||||
return charmap.ISO8859_1.NewEncoder().Bytes([]byte(str))
|
||||
}
|
||||
Reference in New Issue
Block a user