1
0

Feature: add inbounds for flexible binding inbound (#2818)

This commit is contained in:
fuyun 2023-08-03 22:30:08 +08:00 committed by GitHub
parent 10f4d5375a
commit 9e78137768
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 552 additions and 361 deletions

View File

@ -3,6 +3,7 @@ package inbound
import ( import (
"net" "net"
"net/netip" "net/netip"
"strconv"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/context" "github.com/Dreamacro/clash/context"
@ -21,6 +22,7 @@ func NewHTTP(target socks5.Addr, source net.Addr, originTarget net.Addr, conn ne
if originTarget != nil { if originTarget != nil {
if addrPort, err := netip.ParseAddrPort(originTarget.String()); err == nil { if addrPort, err := netip.ParseAddrPort(originTarget.String()); err == nil {
metadata.OriginDst = addrPort metadata.OriginDst = addrPort
metadata.InboundPort = strconv.Itoa(int(addrPort.Port()))
} }
} }
return context.NewConnContext(conn, metadata) return context.NewConnContext(conn, metadata)

View File

@ -4,6 +4,7 @@ import (
"net" "net"
"net/http" "net/http"
"net/netip" "net/netip"
"strconv"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/context" "github.com/Dreamacro/clash/context"
@ -19,6 +20,7 @@ func NewHTTPS(request *http.Request, conn net.Conn) *context.ConnContext {
} }
if addrPort, err := netip.ParseAddrPort(conn.LocalAddr().String()); err == nil { if addrPort, err := netip.ParseAddrPort(conn.LocalAddr().String()); err == nil {
metadata.OriginDst = addrPort metadata.OriginDst = addrPort
metadata.InboundPort = strconv.Itoa(int(addrPort.Port()))
} }
return context.NewConnContext(conn, metadata) return context.NewConnContext(conn, metadata)
} }

View File

@ -3,6 +3,7 @@ package inbound
import ( import (
"net" "net"
"net/netip" "net/netip"
"strconv"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/context" "github.com/Dreamacro/clash/context"
@ -20,6 +21,7 @@ func NewSocket(target socks5.Addr, conn net.Conn, source C.Type) *context.ConnCo
} }
if addrPort, err := netip.ParseAddrPort(conn.LocalAddr().String()); err == nil { if addrPort, err := netip.ParseAddrPort(conn.LocalAddr().String()); err == nil {
metadata.OriginDst = addrPort metadata.OriginDst = addrPort
metadata.InboundPort = strconv.Itoa(int(addrPort.Port()))
} }
return context.NewConnContext(conn, metadata) return context.NewConnContext(conn, metadata)
} }

View File

@ -6,9 +6,9 @@ import (
"net/netip" "net/netip"
"unsafe" "unsafe"
"golang.org/x/sys/windows"
"github.com/Dreamacro/clash/common/pool" "github.com/Dreamacro/clash/common/pool"
"golang.org/x/sys/windows"
) )
var ( var (

View File

@ -28,25 +28,14 @@ import (
// General config // General config
type General struct { type General struct {
Inbound LagecyInbound
Controller Controller
Mode T.TunnelMode `json:"mode"` Authentication []string `json:"authentication"`
LogLevel log.LogLevel `json:"log-level"` Mode T.TunnelMode `json:"mode"`
IPv6 bool `json:"ipv6"` LogLevel log.LogLevel `json:"log-level"`
Interface string `json:"-"` IPv6 bool `json:"ipv6"`
RoutingMark int `json:"-"` Interface string `json:"-"`
} RoutingMark int `json:"-"`
// Inbound
type Inbound struct {
Port int `json:"port"`
SocksPort int `json:"socks-port"`
RedirPort int `json:"redir-port"`
TProxyPort int `json:"tproxy-port"`
MixedPort int `json:"mixed-port"`
Authentication []string `json:"authentication"`
AllowLan bool `json:"allow-lan"`
BindAddress string `json:"bind-address"`
} }
// Controller // Controller
@ -56,6 +45,16 @@ type Controller struct {
Secret string `json:"-"` Secret string `json:"-"`
} }
type LagecyInbound struct {
Port int `json:"port"`
SocksPort int `json:"socks-port"`
RedirPort int `json:"redir-port"`
TProxyPort int `json:"tproxy-port"`
MixedPort int `json:"mixed-port"`
AllowLan bool `json:"allow-lan"`
BindAddress string `json:"bind-address"`
}
// DNS config // DNS config
type DNS struct { type DNS struct {
Enable bool `yaml:"enable"` Enable bool `yaml:"enable"`
@ -98,6 +97,7 @@ type Config struct {
Experimental *Experimental Experimental *Experimental
Hosts *trie.DomainTrie Hosts *trie.DomainTrie
Profile *Profile Profile *Profile
Inbounds []C.Inbound
Rules []C.Rule Rules []C.Rule
Users []auth.AuthUser Users []auth.AuthUser
Proxies map[string]C.Proxy Proxies map[string]C.Proxy
@ -207,6 +207,7 @@ type RawConfig struct {
ProxyProvider map[string]map[string]any `yaml:"proxy-providers"` ProxyProvider map[string]map[string]any `yaml:"proxy-providers"`
Hosts map[string]string `yaml:"hosts"` Hosts map[string]string `yaml:"hosts"`
Inbounds []C.Inbound `yaml:"inbounds"`
DNS RawDNS `yaml:"dns"` DNS RawDNS `yaml:"dns"`
Experimental Experimental `yaml:"experimental"` Experimental Experimental `yaml:"experimental"`
Profile Profile `yaml:"profile"` Profile Profile `yaml:"profile"`
@ -275,6 +276,8 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) {
} }
config.General = general config.General = general
config.Inbounds = rawCfg.Inbounds
proxies, providers, err := parseProxies(rawCfg) proxies, providers, err := parseProxies(rawCfg)
if err != nil { if err != nil {
return nil, err return nil, err
@ -326,7 +329,7 @@ func parseGeneral(cfg *RawConfig) (*General, error) {
} }
return &General{ return &General{
Inbound: Inbound{ LagecyInbound: LagecyInbound{
Port: cfg.Port, Port: cfg.Port,
SocksPort: cfg.SocksPort, SocksPort: cfg.SocksPort,
RedirPort: cfg.RedirPort, RedirPort: cfg.RedirPort,

View File

@ -1,7 +1,92 @@
package constant package constant
import (
"fmt"
"net"
"net/url"
"strconv"
)
type Listener interface { type Listener interface {
RawAddress() string RawAddress() string
Address() string Address() string
Close() error Close() error
} }
type InboundType string
const (
InboundTypeSocks InboundType = "socks"
InboundTypeRedir InboundType = "redir"
InboundTypeTproxy InboundType = "tproxy"
InboundTypeHTTP InboundType = "http"
InboundTypeMixed InboundType = "mixed"
)
var supportInboundTypes = map[InboundType]bool{
InboundTypeSocks: true,
InboundTypeRedir: true,
InboundTypeTproxy: true,
InboundTypeHTTP: true,
InboundTypeMixed: true,
}
type inbound struct {
Type InboundType `json:"type" yaml:"type"`
BindAddress string `json:"bind-address" yaml:"bind-address"`
IsFromPortCfg bool `json:"-" yaml:"-"`
}
// Inbound
type Inbound inbound
// UnmarshalYAML implements yaml.Unmarshaler
func (i *Inbound) UnmarshalYAML(unmarshal func(any) error) error {
var tp string
if err := unmarshal(&tp); err != nil {
var inner inbound
if err := unmarshal(&inner); err != nil {
return err
}
*i = Inbound(inner)
return nil
}
inner, err := parseInbound(tp)
if err != nil {
return err
}
*i = Inbound(*inner)
if !supportInboundTypes[i.Type] {
return fmt.Errorf("not support inbound type: %s", i.Type)
}
_, portStr, err := net.SplitHostPort(i.BindAddress)
if err != nil {
return fmt.Errorf("bind address parse error. addr:%s, err:%v", i.BindAddress, err)
}
port, err := strconv.Atoi(portStr)
if err != nil {
return fmt.Errorf("port not a number. addr:%s", i.BindAddress)
}
if port == 0 {
return fmt.Errorf("invalid bind port. addr:%s", i.BindAddress)
}
return nil
}
func parseInbound(alias string) (*inbound, error) {
u, err := url.Parse(alias)
if err != nil {
return nil, err
}
listenerType := InboundType(u.Scheme)
return &inbound{
Type: listenerType,
BindAddress: u.Host,
}, nil
}
func (i *Inbound) ToAlias() string {
return string(i.Type) + "://" + i.BindAddress
}

View File

@ -69,6 +69,7 @@ type Metadata struct {
DstIP net.IP `json:"destinationIP"` DstIP net.IP `json:"destinationIP"`
SrcPort string `json:"sourcePort"` SrcPort string `json:"sourcePort"`
DstPort string `json:"destinationPort"` DstPort string `json:"destinationPort"`
InboundPort string `json:"inboundPort"`
Host string `json:"host"` Host string `json:"host"`
DNSMode DNSMode `json:"dnsMode"` DNSMode DNSMode `json:"dnsMode"`
ProcessPath string `json:"processPath"` ProcessPath string `json:"processPath"`

View File

@ -10,6 +10,7 @@ const (
SrcIPCIDR SrcIPCIDR
SrcPort SrcPort
DstPort DstPort
InboundPort
Process Process
ProcessPath ProcessPath
IPSet IPSet
@ -36,6 +37,8 @@ func (rt RuleType) String() string {
return "SrcPort" return "SrcPort"
case DstPort: case DstPort:
return "DstPort" return "DstPort"
case InboundPort:
return "InboundPort"
case Process: case Process:
return "Process" return "Process"
case ProcessPath: case ProcessPath:

View File

@ -73,6 +73,7 @@ func ApplyConfig(cfg *config.Config, force bool) {
updateHosts(cfg.Hosts) updateHosts(cfg.Hosts)
updateProfile(cfg) updateProfile(cfg)
updateGeneral(cfg.General, force) updateGeneral(cfg.General, force)
updateInbounds(cfg.Inbounds, force)
updateDNS(cfg.DNS) updateDNS(cfg.DNS)
updateExperimental(cfg) updateExperimental(cfg)
updateTunnels(cfg.Tunnels) updateTunnels(cfg.Tunnels)
@ -86,19 +87,19 @@ func GetGeneral() *config.General {
} }
general := &config.General{ general := &config.General{
Inbound: config.Inbound{ LagecyInbound: config.LagecyInbound{
Port: ports.Port, Port: ports.Port,
SocksPort: ports.SocksPort, SocksPort: ports.SocksPort,
RedirPort: ports.RedirPort, RedirPort: ports.RedirPort,
TProxyPort: ports.TProxyPort, TProxyPort: ports.TProxyPort,
MixedPort: ports.MixedPort, MixedPort: ports.MixedPort,
Authentication: authenticator, AllowLan: listener.AllowLan(),
AllowLan: listener.AllowLan(), BindAddress: listener.BindAddress(),
BindAddress: listener.BindAddress(),
}, },
Mode: tunnel.Mode(), Authentication: authenticator,
LogLevel: log.Level(), Mode: tunnel.Mode(),
IPv6: !resolver.DisableIPv6, LogLevel: log.Level(),
IPv6: !resolver.DisableIPv6,
} }
return general return general
@ -164,6 +165,16 @@ func updateTunnels(tunnels []config.Tunnel) {
listener.PatchTunnel(tunnels, tunnel.TCPIn(), tunnel.UDPIn()) listener.PatchTunnel(tunnels, tunnel.TCPIn(), tunnel.UDPIn())
} }
func updateInbounds(inbounds []C.Inbound, force bool) {
if !force {
return
}
tcpIn := tunnel.TCPIn()
udpIn := tunnel.UDPIn()
listener.ReCreateListeners(inbounds, tcpIn, udpIn)
}
func updateGeneral(general *config.General, force bool) { func updateGeneral(general *config.General, force bool) {
log.SetLevel(general.LogLevel) log.SetLevel(general.LogLevel)
tunnel.SetMode(general.Mode) tunnel.SetMode(general.Mode)
@ -184,14 +195,14 @@ func updateGeneral(general *config.General, force bool) {
bindAddress := general.BindAddress bindAddress := general.BindAddress
listener.SetBindAddress(bindAddress) listener.SetBindAddress(bindAddress)
tcpIn := tunnel.TCPIn() ports := listener.Ports{
udpIn := tunnel.UDPIn() Port: general.Port,
SocksPort: general.SocksPort,
listener.ReCreateHTTP(general.Port, tcpIn) RedirPort: general.RedirPort,
listener.ReCreateSocks(general.SocksPort, tcpIn, udpIn) TProxyPort: general.TProxyPort,
listener.ReCreateRedir(general.RedirPort, tcpIn, udpIn) MixedPort: general.MixedPort,
listener.ReCreateTProxy(general.TProxyPort, tcpIn, udpIn) }
listener.ReCreateMixed(general.MixedPort, tcpIn, udpIn) listener.ReCreatePortsListeners(ports, tunnel.TCPIn(), tunnel.UDPIn())
} }
func updateUsers(users []auth.AuthUser) { func updateUsers(users []auth.AuthUser) {

View File

@ -6,14 +6,15 @@ import (
"github.com/Dreamacro/clash/component/resolver" "github.com/Dreamacro/clash/component/resolver"
"github.com/Dreamacro/clash/config" "github.com/Dreamacro/clash/config"
"github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/hub/executor" "github.com/Dreamacro/clash/hub/executor"
P "github.com/Dreamacro/clash/listener" "github.com/Dreamacro/clash/listener"
"github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/log"
"github.com/Dreamacro/clash/tunnel" "github.com/Dreamacro/clash/tunnel"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-chi/render" "github.com/go-chi/render"
"github.com/samber/lo"
) )
func configRouter() http.Handler { func configRouter() http.Handler {
@ -29,14 +30,6 @@ func getConfigs(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, general) render.JSON(w, r, general)
} }
func pointerOrDefault(p *int, def int) int {
if p != nil {
return *p
}
return def
}
func patchConfigs(w http.ResponseWriter, r *http.Request) { func patchConfigs(w http.ResponseWriter, r *http.Request) {
general := struct { general := struct {
Port *int `json:"port"` Port *int `json:"port"`
@ -56,25 +49,6 @@ func patchConfigs(w http.ResponseWriter, r *http.Request) {
return return
} }
if general.AllowLan != nil {
P.SetAllowLan(*general.AllowLan)
}
if general.BindAddress != nil {
P.SetBindAddress(*general.BindAddress)
}
ports := P.GetPorts()
tcpIn := tunnel.TCPIn()
udpIn := tunnel.UDPIn()
P.ReCreateHTTP(pointerOrDefault(general.Port, ports.Port), tcpIn)
P.ReCreateSocks(pointerOrDefault(general.SocksPort, ports.SocksPort), tcpIn, udpIn)
P.ReCreateRedir(pointerOrDefault(general.RedirPort, ports.RedirPort), tcpIn, udpIn)
P.ReCreateTProxy(pointerOrDefault(general.TProxyPort, ports.TProxyPort), tcpIn, udpIn)
P.ReCreateMixed(pointerOrDefault(general.MixedPort, ports.MixedPort), tcpIn, udpIn)
if general.Mode != nil { if general.Mode != nil {
tunnel.SetMode(*general.Mode) tunnel.SetMode(*general.Mode)
} }
@ -87,6 +61,23 @@ func patchConfigs(w http.ResponseWriter, r *http.Request) {
resolver.DisableIPv6 = !*general.IPv6 resolver.DisableIPv6 = !*general.IPv6
} }
if general.AllowLan != nil {
listener.SetAllowLan(*general.AllowLan)
}
if general.BindAddress != nil {
listener.SetBindAddress(*general.BindAddress)
}
ports := listener.GetPorts()
ports.Port = lo.FromPtrOr(general.Port, ports.Port)
ports.SocksPort = lo.FromPtrOr(general.SocksPort, ports.SocksPort)
ports.RedirPort = lo.FromPtrOr(general.RedirPort, ports.RedirPort)
ports.TProxyPort = lo.FromPtrOr(general.TProxyPort, ports.TProxyPort)
ports.MixedPort = lo.FromPtrOr(general.MixedPort, ports.MixedPort)
listener.ReCreatePortsListeners(*ports, tunnel.TCPIn(), tunnel.UDPIn())
render.NoContent(w, r) render.NoContent(w, r)
} }
@ -114,7 +105,7 @@ func updateConfigs(w http.ResponseWriter, r *http.Request) {
} }
} else { } else {
if req.Path == "" { if req.Path == "" {
req.Path = constant.Path.Config() req.Path = C.Path.Config()
} }
if !filepath.IsAbs(req.Path) { if !filepath.IsAbs(req.Path) {
render.Status(r, http.StatusBadRequest) render.Status(r, http.StatusBadRequest)

39
hub/route/inbounds.go Normal file
View File

@ -0,0 +1,39 @@
package route
import (
"net/http"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/listener"
"github.com/Dreamacro/clash/tunnel"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
)
func inboundRouter() http.Handler {
r := chi.NewRouter()
r.Get("/", getInbounds)
r.Put("/", updateInbounds)
return r
}
func getInbounds(w http.ResponseWriter, r *http.Request) {
inbounds := listener.GetInbounds()
render.JSON(w, r, render.M{
"inbounds": inbounds,
})
}
func updateInbounds(w http.ResponseWriter, r *http.Request) {
var req []C.Inbound
if err := render.DecodeJSON(r.Body, &req); err != nil {
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, ErrBadRequest)
return
}
tcpIn := tunnel.TCPIn()
udpIn := tunnel.UDPIn()
listener.ReCreateListeners(req, tcpIn, udpIn)
render.NoContent(w, r)
}

View File

@ -69,6 +69,7 @@ func Start(addr string, secret string) {
r.Get("/traffic", traffic) r.Get("/traffic", traffic)
r.Get("/version", version) r.Get("/version", version)
r.Mount("/configs", configRouter()) r.Mount("/configs", configRouter())
r.Mount("/inbounds", inboundRouter())
r.Mount("/proxies", proxyRouter()) r.Mount("/proxies", proxyRouter())
r.Mount("/rules", ruleRouter()) r.Mount("/rules", ruleRouter())
r.Mount("/connections", connectionRouter()) r.Mount("/connections", connectionRouter())

View File

@ -29,11 +29,11 @@ func (l *Listener) Close() error {
return l.listener.Close() return l.listener.Close()
} }
func New(addr string, in chan<- C.ConnContext) (*Listener, error) { func New(addr string, in chan<- C.ConnContext) (C.Listener, error) {
return NewWithAuthenticate(addr, in, true) return NewWithAuthenticate(addr, in, true)
} }
func NewWithAuthenticate(addr string, in chan<- C.ConnContext, authenticate bool) (*Listener, error) { func NewWithAuthenticate(addr string, in chan<- C.ConnContext, authenticate bool) (C.Listener, error) {
l, err := net.Listen("tcp", addr) l, err := net.Listen("tcp", addr)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -25,25 +25,15 @@ var (
allowLan = false allowLan = false
bindAddress = "*" bindAddress = "*"
socksListener *socks.Listener tcpListeners = map[C.Inbound]C.Listener{}
socksUDPListener *socks.UDPListener udpListeners = map[C.Inbound]C.Listener{}
httpListener *http.Listener
redirListener *redir.Listener
redirUDPListener *tproxy.UDPListener
tproxyListener *tproxy.Listener
tproxyUDPListener *tproxy.UDPListener
mixedListener *mixed.Listener
mixedUDPLister *socks.UDPListener
tunnelTCPListeners = map[string]*tunnel.Listener{} tunnelTCPListeners = map[string]*tunnel.Listener{}
tunnelUDPListeners = map[string]*tunnel.PacketConn{} tunnelUDPListeners = map[string]*tunnel.PacketConn{}
// lock for recreate function // lock for recreate function
socksMux sync.Mutex recreateMux sync.Mutex
httpMux sync.Mutex tunnelMux sync.Mutex
redirMux sync.Mutex
tproxyMux sync.Mutex
mixedMux sync.Mutex
tunnelMux sync.Mutex
) )
type Ports struct { type Ports struct {
@ -54,6 +44,26 @@ type Ports struct {
MixedPort int `json:"mixed-port"` MixedPort int `json:"mixed-port"`
} }
var tcpListenerCreators = map[C.InboundType]tcpListenerCreator{
C.InboundTypeHTTP: http.New,
C.InboundTypeSocks: socks.New,
C.InboundTypeRedir: redir.New,
C.InboundTypeTproxy: tproxy.New,
C.InboundTypeMixed: mixed.New,
}
var udpListenerCreators = map[C.InboundType]udpListenerCreator{
C.InboundTypeSocks: socks.NewUDP,
C.InboundTypeRedir: tproxy.NewUDP,
C.InboundTypeTproxy: tproxy.NewUDP,
C.InboundTypeMixed: socks.NewUDP,
}
type (
tcpListenerCreator func(addr string, tcpIn chan<- C.ConnContext) (C.Listener, error)
udpListenerCreator func(addr string, udpIn chan<- *inbound.PacketAdapter) (C.Listener, error)
)
func AllowLan() bool { func AllowLan() bool {
return allowLan return allowLan
} }
@ -70,243 +80,119 @@ func SetBindAddress(host string) {
bindAddress = host bindAddress = host
} }
func ReCreateHTTP(port int, tcpIn chan<- C.ConnContext) { func createListener(inbound C.Inbound, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.PacketAdapter) {
httpMux.Lock() addr := inbound.BindAddress
defer httpMux.Unlock()
var err error
defer func() {
if err != nil {
log.Errorln("Start HTTP server error: %s", err.Error())
}
}()
addr := genAddr(bindAddress, port, allowLan)
if httpListener != nil {
if httpListener.RawAddress() == addr {
return
}
httpListener.Close()
httpListener = nil
}
if portIsZero(addr) { if portIsZero(addr) {
return return
} }
tcpCreator := tcpListenerCreators[inbound.Type]
httpListener, err = http.New(addr, tcpIn) udpCreator := udpListenerCreators[inbound.Type]
if err != nil { if tcpCreator == nil && udpCreator == nil {
log.Errorln("inbound type %s not support.", inbound.Type)
return return
} }
if tcpCreator != nil {
log.Infoln("HTTP proxy listening at: %s", httpListener.Address()) tcpListener, err := tcpCreator(addr, tcpIn)
if err != nil {
log.Errorln("create addr %s tcp listener error. err:%v", addr, err)
return
}
tcpListeners[inbound] = tcpListener
}
if udpCreator != nil {
udpListener, err := udpCreator(addr, udpIn)
if err != nil {
log.Errorln("create addr %s udp listener error. err:%v", addr, err)
return
}
udpListeners[inbound] = udpListener
}
log.Infoln("inbound %s create success.", inbound.ToAlias())
} }
func ReCreateSocks(port int, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.PacketAdapter) { func closeListener(inbound C.Inbound) {
socksMux.Lock() listener := tcpListeners[inbound]
defer socksMux.Unlock() if listener != nil {
if err := listener.Close(); err != nil {
var err error log.Errorln("close tcp address `%s` error. err:%s", inbound.ToAlias(), err.Error())
defer func() {
if err != nil {
log.Errorln("Start SOCKS server error: %s", err.Error())
} }
}() delete(tcpListeners, inbound)
}
addr := genAddr(bindAddress, port, allowLan) listener = udpListeners[inbound]
if listener != nil {
shouldTCPIgnore := false if err := listener.Close(); err != nil {
shouldUDPIgnore := false log.Errorln("close udp address `%s` error. err:%s", inbound.ToAlias(), err.Error())
if socksListener != nil {
if socksListener.RawAddress() != addr {
socksListener.Close()
socksListener = nil
} else {
shouldTCPIgnore = true
} }
delete(udpListeners, inbound)
} }
if socksUDPListener != nil {
if socksUDPListener.RawAddress() != addr {
socksUDPListener.Close()
socksUDPListener = nil
} else {
shouldUDPIgnore = true
}
}
if shouldTCPIgnore && shouldUDPIgnore {
return
}
if portIsZero(addr) {
return
}
tcpListener, err := socks.New(addr, tcpIn)
if err != nil {
return
}
udpListener, err := socks.NewUDP(addr, udpIn)
if err != nil {
tcpListener.Close()
return
}
socksListener = tcpListener
socksUDPListener = udpListener
log.Infoln("SOCKS proxy listening at: %s", socksListener.Address())
} }
func ReCreateRedir(port int, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.PacketAdapter) { func getNeedCloseAndCreateInbound(originInbounds []C.Inbound, newInbounds []C.Inbound) ([]C.Inbound, []C.Inbound) {
redirMux.Lock() needCloseMap := map[C.Inbound]bool{}
defer redirMux.Unlock() needClose := []C.Inbound{}
needCreate := []C.Inbound{}
var err error for _, inbound := range originInbounds {
defer func() { needCloseMap[inbound] = true
if err != nil { }
log.Errorln("Start Redir server error: %s", err.Error()) for _, inbound := range newInbounds {
if needCloseMap[inbound] {
delete(needCloseMap, inbound)
} else {
needCreate = append(needCreate, inbound)
} }
}()
addr := genAddr(bindAddress, port, allowLan)
if redirListener != nil {
if redirListener.RawAddress() == addr {
return
}
redirListener.Close()
redirListener = nil
} }
for inbound := range needCloseMap {
if redirUDPListener != nil { needClose = append(needClose, inbound)
if redirUDPListener.RawAddress() == addr {
return
}
redirUDPListener.Close()
redirUDPListener = nil
} }
return needClose, needCreate
if portIsZero(addr) {
return
}
redirListener, err = redir.New(addr, tcpIn)
if err != nil {
return
}
redirUDPListener, err = tproxy.NewUDP(addr, udpIn)
if err != nil {
log.Warnln("Failed to start Redir UDP Listener: %s", err)
}
log.Infoln("Redirect proxy listening at: %s", redirListener.Address())
} }
func ReCreateTProxy(port int, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.PacketAdapter) { // only recreate inbound config listener
tproxyMux.Lock() func ReCreateListeners(inbounds []C.Inbound, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.PacketAdapter) {
defer tproxyMux.Unlock() newInbounds := []C.Inbound{}
newInbounds = append(newInbounds, inbounds...)
var err error for _, inbound := range getInbounds() {
defer func() { if inbound.IsFromPortCfg {
if err != nil { newInbounds = append(newInbounds, inbound)
log.Errorln("Start TProxy server error: %s", err.Error())
} }
}()
addr := genAddr(bindAddress, port, allowLan)
if tproxyListener != nil {
if tproxyListener.RawAddress() == addr {
return
}
tproxyListener.Close()
tproxyListener = nil
} }
reCreateListeners(newInbounds, tcpIn, udpIn)
if tproxyUDPListener != nil {
if tproxyUDPListener.RawAddress() == addr {
return
}
tproxyUDPListener.Close()
tproxyUDPListener = nil
}
if portIsZero(addr) {
return
}
tproxyListener, err = tproxy.New(addr, tcpIn)
if err != nil {
return
}
tproxyUDPListener, err = tproxy.NewUDP(addr, udpIn)
if err != nil {
log.Warnln("Failed to start TProxy UDP Listener: %s", err)
}
log.Infoln("TProxy server listening at: %s", tproxyListener.Address())
} }
func ReCreateMixed(port int, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.PacketAdapter) { // only recreate ports config listener
mixedMux.Lock() func ReCreatePortsListeners(ports Ports, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.PacketAdapter) {
defer mixedMux.Unlock() newInbounds := []C.Inbound{}
newInbounds = append(newInbounds, GetInbounds()...)
newInbounds = addPortInbound(newInbounds, C.InboundTypeHTTP, ports.Port)
newInbounds = addPortInbound(newInbounds, C.InboundTypeSocks, ports.SocksPort)
newInbounds = addPortInbound(newInbounds, C.InboundTypeRedir, ports.RedirPort)
newInbounds = addPortInbound(newInbounds, C.InboundTypeTproxy, ports.TProxyPort)
newInbounds = addPortInbound(newInbounds, C.InboundTypeMixed, ports.MixedPort)
reCreateListeners(newInbounds, tcpIn, udpIn)
}
var err error func addPortInbound(inbounds []C.Inbound, inboundType C.InboundType, port int) []C.Inbound {
defer func() { if port != 0 {
if err != nil { inbounds = append(inbounds, C.Inbound{
log.Errorln("Start Mixed(http+socks) server error: %s", err.Error()) Type: inboundType,
} BindAddress: genAddr(bindAddress, port, allowLan),
}() IsFromPortCfg: true,
})
addr := genAddr(bindAddress, port, allowLan)
shouldTCPIgnore := false
shouldUDPIgnore := false
if mixedListener != nil {
if mixedListener.RawAddress() != addr {
mixedListener.Close()
mixedListener = nil
} else {
shouldTCPIgnore = true
}
}
if mixedUDPLister != nil {
if mixedUDPLister.RawAddress() != addr {
mixedUDPLister.Close()
mixedUDPLister = nil
} else {
shouldUDPIgnore = true
}
} }
return inbounds
}
if shouldTCPIgnore && shouldUDPIgnore { func reCreateListeners(inbounds []C.Inbound, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.PacketAdapter) {
return recreateMux.Lock()
defer recreateMux.Unlock()
needClose, needCreate := getNeedCloseAndCreateInbound(getInbounds(), inbounds)
for _, inbound := range needClose {
closeListener(inbound)
} }
for _, inbound := range needCreate {
if portIsZero(addr) { createListener(inbound, tcpIn, udpIn)
return
} }
mixedListener, err = mixed.New(addr, tcpIn)
if err != nil {
return
}
mixedUDPLister, err = socks.NewUDP(addr, udpIn)
if err != nil {
mixedListener.Close()
return
}
log.Infoln("Mixed(http+socks) proxy listening at: %s", mixedListener.Address())
} }
func PatchTunnel(tunnels []config.Tunnel, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.PacketAdapter) { func PatchTunnel(tunnels []config.Tunnel, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.PacketAdapter) {
@ -398,43 +284,55 @@ func PatchTunnel(tunnels []config.Tunnel, tcpIn chan<- C.ConnContext, udpIn chan
} }
} }
func GetInbounds() []C.Inbound {
return lo.Filter(getInbounds(), func(inbound C.Inbound, idx int) bool {
return !inbound.IsFromPortCfg
})
}
// GetInbounds return the inbounds of proxy servers
func getInbounds() []C.Inbound {
var inbounds []C.Inbound
for inbound := range tcpListeners {
inbounds = append(inbounds, inbound)
}
for inbound := range udpListeners {
if _, ok := tcpListeners[inbound]; !ok {
inbounds = append(inbounds, inbound)
}
}
return inbounds
}
// GetPorts return the ports of proxy servers // GetPorts return the ports of proxy servers
func GetPorts() *Ports { func GetPorts() *Ports {
ports := &Ports{} ports := &Ports{}
for _, inbound := range getInbounds() {
if httpListener != nil { fillPort(inbound, ports)
_, portStr, _ := net.SplitHostPort(httpListener.Address())
port, _ := strconv.Atoi(portStr)
ports.Port = port
} }
if socksListener != nil {
_, portStr, _ := net.SplitHostPort(socksListener.Address())
port, _ := strconv.Atoi(portStr)
ports.SocksPort = port
}
if redirListener != nil {
_, portStr, _ := net.SplitHostPort(redirListener.Address())
port, _ := strconv.Atoi(portStr)
ports.RedirPort = port
}
if tproxyListener != nil {
_, portStr, _ := net.SplitHostPort(tproxyListener.Address())
port, _ := strconv.Atoi(portStr)
ports.TProxyPort = port
}
if mixedListener != nil {
_, portStr, _ := net.SplitHostPort(mixedListener.Address())
port, _ := strconv.Atoi(portStr)
ports.MixedPort = port
}
return ports return ports
} }
func fillPort(inbound C.Inbound, ports *Ports) {
if inbound.IsFromPortCfg {
port := getPort(inbound.BindAddress)
switch inbound.Type {
case C.InboundTypeHTTP:
ports.Port = port
case C.InboundTypeSocks:
ports.SocksPort = port
case C.InboundTypeTproxy:
ports.TProxyPort = port
case C.InboundTypeRedir:
ports.RedirPort = port
case C.InboundTypeMixed:
ports.MixedPort = port
default:
// do nothing
}
}
}
func portIsZero(addr string) bool { func portIsZero(addr string) bool {
_, port, err := net.SplitHostPort(addr) _, port, err := net.SplitHostPort(addr)
if port == "0" || port == "" || err != nil { if port == "0" || port == "" || err != nil {
@ -453,3 +351,15 @@ func genAddr(host string, port int, allowLan bool) string {
return fmt.Sprintf("127.0.0.1:%d", port) return fmt.Sprintf("127.0.0.1:%d", port)
} }
func getPort(addr string) int {
_, portStr, err := net.SplitHostPort(addr)
if err != nil {
return 0
}
port, err := strconv.Atoi(portStr)
if err != nil {
return 0
}
return port
}

View File

@ -35,7 +35,7 @@ func (l *Listener) Close() error {
return l.listener.Close() return l.listener.Close()
} }
func New(addr string, in chan<- C.ConnContext) (*Listener, error) { func New(addr string, in chan<- C.ConnContext) (C.Listener, error) {
l, err := net.Listen("tcp", addr) l, err := net.Listen("tcp", addr)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -29,7 +29,7 @@ func (l *Listener) Close() error {
return l.listener.Close() return l.listener.Close()
} }
func New(addr string, in chan<- C.ConnContext) (*Listener, error) { func New(addr string, in chan<- C.ConnContext) (C.Listener, error) {
l, err := net.Listen("tcp", addr) l, err := net.Listen("tcp", addr)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -34,7 +34,7 @@ func (l *Listener) Close() error {
return l.listener.Close() return l.listener.Close()
} }
func New(addr string, in chan<- C.ConnContext) (*Listener, error) { func New(addr string, in chan<- C.ConnContext) (C.Listener, error) {
l, err := net.Listen("tcp", addr) l, err := net.Listen("tcp", addr)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -33,7 +33,7 @@ func (l *UDPListener) Close() error {
return l.packetConn.Close() return l.packetConn.Close()
} }
func NewUDP(addr string, in chan<- *inbound.PacketAdapter) (*UDPListener, error) { func NewUDP(addr string, in chan<- *inbound.PacketAdapter) (C.Listener, error) {
l, err := net.ListenPacket("udp", addr) l, err := net.ListenPacket("udp", addr)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -36,7 +36,7 @@ func (l *Listener) handleTProxy(conn net.Conn, in chan<- C.ConnContext) {
in <- inbound.NewSocket(target, conn, C.TPROXY) in <- inbound.NewSocket(target, conn, C.TPROXY)
} }
func New(addr string, in chan<- C.ConnContext) (*Listener, error) { func New(addr string, in chan<- C.ConnContext) (C.Listener, error) {
l, err := net.Listen("tcp", addr) l, err := net.Listen("tcp", addr)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -32,7 +32,7 @@ func (l *UDPListener) Close() error {
return l.packetConn.Close() return l.packetConn.Close()
} }
func NewUDP(addr string, in chan<- *inbound.PacketAdapter) (*UDPListener, error) { func NewUDP(addr string, in chan<- *inbound.PacketAdapter) (C.Listener, error) {
l, err := net.ListenPacket("udp", addr) l, err := net.ListenPacket("udp", addr)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -28,9 +28,11 @@ func ParseRule(tp, payload, target string, params []string) (C.Rule, error) {
case "SRC-IP-CIDR": case "SRC-IP-CIDR":
parsed, parseErr = NewIPCIDR(payload, target, WithIPCIDRSourceIP(true), WithIPCIDRNoResolve(true)) parsed, parseErr = NewIPCIDR(payload, target, WithIPCIDRSourceIP(true), WithIPCIDRNoResolve(true))
case "SRC-PORT": case "SRC-PORT":
parsed, parseErr = NewPort(payload, target, true) parsed, parseErr = NewPort(payload, target, PortTypeSrc)
case "DST-PORT": case "DST-PORT":
parsed, parseErr = NewPort(payload, target, false) parsed, parseErr = NewPort(payload, target, PortTypeDest)
case "INBOUND-PORT":
parsed, parseErr = NewPort(payload, target, PortTypeInbound)
case "PROCESS-NAME": case "PROCESS-NAME":
parsed, parseErr = NewProcess(payload, target, true) parsed, parseErr = NewProcess(payload, target, true)
case "PROCESS-PATH": case "PROCESS-PATH":

View File

@ -1,29 +1,50 @@
package rules package rules
import ( import (
"fmt"
"strconv" "strconv"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
) )
type PortType int
const (
PortTypeSrc PortType = iota
PortTypeDest
PortTypeInbound
)
type Port struct { type Port struct {
adapter string adapter string
port string port string
isSource bool portType PortType
} }
func (p *Port) RuleType() C.RuleType { func (p *Port) RuleType() C.RuleType {
if p.isSource { switch p.portType {
case PortTypeSrc:
return C.SrcPort return C.SrcPort
case PortTypeDest:
return C.DstPort
case PortTypeInbound:
return C.InboundPort
default:
panic(fmt.Errorf("unknown port type: %v", p.portType))
} }
return C.DstPort
} }
func (p *Port) Match(metadata *C.Metadata) bool { func (p *Port) Match(metadata *C.Metadata) bool {
if p.isSource { switch p.portType {
case PortTypeSrc:
return metadata.SrcPort == p.port return metadata.SrcPort == p.port
case PortTypeDest:
return metadata.DstPort == p.port
case PortTypeInbound:
return metadata.InboundPort == p.port
default:
panic(fmt.Errorf("unknown port type: %v", p.portType))
} }
return metadata.DstPort == p.port
} }
func (p *Port) Adapter() string { func (p *Port) Adapter() string {
@ -42,7 +63,7 @@ func (p *Port) ShouldFindProcess() bool {
return false return false
} }
func NewPort(port string, adapter string, isSource bool) (*Port, error) { func NewPort(port string, adapter string, portType PortType) (*Port, error) {
_, err := strconv.ParseUint(port, 10, 16) _, err := strconv.ParseUint(port, 10, 16)
if err != nil { if err != nil {
return nil, errPayload return nil, errPayload
@ -50,6 +71,6 @@ func NewPort(port string, adapter string, isSource bool) (*Port, error) {
return &Port{ return &Port{
adapter: adapter, adapter: adapter,
port: port, port: port,
isSource: isSource, portType: portType,
}, nil }, nil
} }

View File

@ -209,23 +209,27 @@ func newLargeDataPair() (chan hashPair, chan hashPair, func(t *testing.T) error)
return pingCh, pongCh, test return pingCh, pongCh, test
} }
func testPingPongWithSocksPort(t *testing.T, port int) { func testPingPongWithSocksPort(t *testing.T, port int) error {
l, err := Listen("tcp", ":10001")
require.NoError(t, err)
defer l.Close()
pingCh, pongCh, test := newPingPongPair() pingCh, pongCh, test := newPingPongPair()
go func() { go func() {
l, err := Listen("tcp", ":10001")
require.NoError(t, err)
defer l.Close()
c, err := l.Accept() c, err := l.Accept()
require.NoError(t, err) if err != nil {
return
}
buf := make([]byte, 4) buf := make([]byte, 4)
_, err = io.ReadFull(c, buf) if _, err = io.ReadFull(c, buf); err != nil {
require.NoError(t, err) return
}
pingCh <- buf pingCh <- buf
_, err = c.Write([]byte("pong")) if _, err = c.Write([]byte("pong")); err != nil {
require.NoError(t, err) return
}
}() }()
go func() { go func() {
@ -233,20 +237,23 @@ func testPingPongWithSocksPort(t *testing.T, port int) {
require.NoError(t, err) require.NoError(t, err)
defer c.Close() defer c.Close()
_, err = socks5.ClientHandshake(c, socks5.ParseAddr("127.0.0.1:10001"), socks5.CmdConnect, nil) if _, err = socks5.ClientHandshake(c, socks5.ParseAddr("127.0.0.1:10001"), socks5.CmdConnect, nil); err != nil {
require.NoError(t, err) return
}
_, err = c.Write([]byte("ping")) if _, err = c.Write([]byte("ping")); err != nil {
require.NoError(t, err) return
}
buf := make([]byte, 4) buf := make([]byte, 4)
_, err = io.ReadFull(c, buf) if _, err = io.ReadFull(c, buf); err != nil {
require.NoError(t, err) return
}
pongCh <- buf pongCh <- buf
}() }()
test(t) return test(t)
} }
func testPingPongWithConn(t *testing.T, c net.Conn) error { func testPingPongWithConn(t *testing.T, c net.Conn) error {
@ -655,7 +662,7 @@ log-level: silent
defer cleanup() defer cleanup()
require.True(t, TCPing(net.JoinHostPort("127.0.0.1", "10000"))) require.True(t, TCPing(net.JoinHostPort("127.0.0.1", "10000")))
testPingPongWithSocksPort(t, 10000) require.NoError(t, testPingPongWithSocksPort(t, 10000))
} }
func Benchmark_Direct(b *testing.B) { func Benchmark_Direct(b *testing.B) {

78
test/listener_test.go Normal file
View File

@ -0,0 +1,78 @@
package main
import (
"net"
"strconv"
"testing"
"time"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/listener"
"github.com/Dreamacro/clash/tunnel"
"github.com/stretchr/testify/require"
)
func TestClash_Listener(t *testing.T) {
basic := `
log-level: silent
port: 7890
socks-port: 7891
redir-port: 7892
tproxy-port: 7893
mixed-port: 7894
`
err := parseAndApply(basic)
require.NoError(t, err)
defer cleanup()
time.Sleep(waitTime)
for i := 7890; i <= 7894; i++ {
require.True(t, TCPing(net.JoinHostPort("127.0.0.1", strconv.Itoa(i))), "tcp port %d", i)
}
}
func TestClash_ListenerCreate(t *testing.T) {
basic := `
log-level: silent
`
err := parseAndApply(basic)
require.NoError(t, err)
defer cleanup()
time.Sleep(waitTime)
tcpIn := tunnel.TCPIn()
udpIn := tunnel.UDPIn()
ports := listener.Ports{
Port: 7890,
}
listener.ReCreatePortsListeners(ports, tcpIn, udpIn)
require.True(t, TCPing("127.0.0.1:7890"))
require.Equal(t, ports, *listener.GetPorts())
inbounds := []C.Inbound{
{
Type: C.InboundTypeHTTP,
BindAddress: "127.0.0.1:7891",
},
}
listener.ReCreateListeners(inbounds, tcpIn, udpIn)
require.True(t, TCPing("127.0.0.1:7890"))
require.Equal(t, ports, *listener.GetPorts())
require.True(t, TCPing("127.0.0.1:7891"))
require.Equal(t, len(inbounds), len(listener.GetInbounds()))
ports.Port = 0
ports.SocksPort = 7892
listener.ReCreatePortsListeners(ports, tcpIn, udpIn)
require.False(t, TCPing("127.0.0.1:7890"))
require.True(t, TCPing("127.0.0.1:7892"))
require.Equal(t, ports, *listener.GetPorts())
require.True(t, TCPing("127.0.0.1:7891"))
require.Equal(t, len(inbounds), len(listener.GetInbounds()))
}

33
test/rule_test.go Normal file
View File

@ -0,0 +1,33 @@
package main
import (
"net"
"testing"
"github.com/stretchr/testify/require"
)
func TestClash_RuleInbound(t *testing.T) {
basic := `
socks-port: 7890
inbounds:
- socks://127.0.0.1:7891
- type: socks
bind-address: 127.0.0.1:7892
rules:
- INBOUND-PORT,7891,REJECT
log-level: silent
`
err := parseAndApply(basic)
require.NoError(t, err)
defer cleanup()
require.True(t, TCPing(net.JoinHostPort("127.0.0.1", "7890")))
require.True(t, TCPing(net.JoinHostPort("127.0.0.1", "7891")))
require.True(t, TCPing(net.JoinHostPort("127.0.0.1", "7892")))
require.Error(t, testPingPongWithSocksPort(t, 7891))
require.NoError(t, testPingPongWithSocksPort(t, 7890))
require.NoError(t, testPingPongWithSocksPort(t, 7892))
}