Compare commits

...

8 Commits

16 changed files with 255 additions and 81 deletions

View File

@ -5,7 +5,7 @@
<br>
</h1>
<h4 align="center">A rule based tunnel in Go.</h4>
<h4 align="center">A rule-based tunnel in Go.</h4>
<p align="center">
<a href="https://travis-ci.org/Dreamacro/clash">
@ -96,6 +96,10 @@ log-level: info
# A RESTful API for clash
external-controller: 127.0.0.1:9090
# you can put the static web resource (such as clash-dashboard) to a directory, and clash would serve in `${API}/ui`
# input is a relative path to the configuration directory or an absolute path
# external-ui: folder
# Secret for RESTful API (Optional)
# secret: ""
@ -126,8 +130,8 @@ Proxy:
- { name: "vmess", type: vmess, server: server, port: 443, uuid: uuid, alterId: 32, cipher: auto, tls: true }
# with tls and skip-cert-verify
- { name: "vmess", type: vmess, server: server, port: 443, uuid: uuid, alterId: 32, cipher: auto, tls: true, skip-cert-verify: true }
# with ws
- { name: "vmess", type: vmess, server: server, port: 443, uuid: uuid, alterId: 32, cipher: auto, network: ws, ws-path: /path }
# with ws-path and ws-headers
- { name: "vmess", type: vmess, server: server, port: 443, uuid: uuid, alterId: 32, cipher: auto, network: ws, ws-path: /path, ws-headers: { Host: v2ray.com } }
# with ws + tls
- { name: "vmess", type: vmess, server: server, port: 443, uuid: uuid, alterId: 32, cipher: auto, network: ws, ws-path: /path, tls: true }

View File

@ -41,6 +41,7 @@ type VmessOption struct {
TLS bool `proxy:"tls,omitempty"`
Network string `proxy:"network,omitempty"`
WSPath string `proxy:"ws-path,omitempty"`
WSHeaders map[string]string `proxy:"ws-headers,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
}
@ -79,6 +80,7 @@ func NewVmess(option VmessOption) (*Vmess, error) {
Port: strconv.Itoa(option.Port),
NetWork: option.Network,
WebSocketPath: option.WSPath,
WebSocketHeaders: option.WSHeaders,
SkipCertVerify: option.SkipCertVerify,
SessionCacahe: getClientSessionCache(),
})

View File

@ -1,5 +1,7 @@
package structure
// references: https://github.com/mitchellh/mapstructure
import (
"fmt"
"reflect"
@ -70,6 +72,8 @@ func (d *Decoder) decode(name string, data interface{}, val reflect.Value) error
return d.decodeBool(name, data, val)
case reflect.Slice:
return d.decodeSlice(name, data, val)
case reflect.Map:
return d.decodeMap(name, data, val)
default:
return fmt.Errorf("type %s not support", val.Kind().String())
}
@ -158,3 +162,70 @@ func (d *Decoder) decodeSlice(name string, data interface{}, val reflect.Value)
val.Set(valSlice)
return nil
}
func (d *Decoder) decodeMap(name string, data interface{}, val reflect.Value) error {
valType := val.Type()
valKeyType := valType.Key()
valElemType := valType.Elem()
valMap := val
if valMap.IsNil() {
mapType := reflect.MapOf(valKeyType, valElemType)
valMap = reflect.MakeMap(mapType)
}
dataVal := reflect.Indirect(reflect.ValueOf(data))
if dataVal.Kind() != reflect.Map {
return fmt.Errorf("'%s' expected a map, got '%s'", name, dataVal.Kind())
}
return d.decodeMapFromMap(name, dataVal, val, valMap)
}
func (d *Decoder) decodeMapFromMap(name string, dataVal reflect.Value, val reflect.Value, valMap reflect.Value) error {
valType := val.Type()
valKeyType := valType.Key()
valElemType := valType.Elem()
errors := make([]string, 0)
if dataVal.Len() == 0 {
if dataVal.IsNil() {
if !val.IsNil() {
val.Set(dataVal)
}
} else {
val.Set(valMap)
}
return nil
}
for _, k := range dataVal.MapKeys() {
fieldName := fmt.Sprintf("%s[%s]", name, k)
currentKey := reflect.Indirect(reflect.New(valKeyType))
if err := d.decode(fieldName, k.Interface(), currentKey); err != nil {
errors = append(errors, err.Error())
continue
}
v := dataVal.MapIndex(k).Interface()
currentVal := reflect.Indirect(reflect.New(valElemType))
if err := d.decode(fieldName, v, currentVal); err != nil {
errors = append(errors, err.Error())
continue
}
valMap.SetMapIndex(currentKey, currentVal)
}
val.Set(valMap)
if len(errors) > 0 {
return fmt.Errorf(strings.Join(errors, ","))
}
return nil
}

View File

@ -83,6 +83,7 @@ type Config struct {
Port string
NetWork string
WebSocketPath string
WebSocketHeaders map[string]string
SkipCertVerify bool
SessionCacahe tls.ClientSessionCache
}
@ -149,6 +150,7 @@ func NewClient(config Config) (*Client, error) {
wsConfig = &websocketConfig{
host: host,
path: config.WebSocketPath,
headers: config.WebSocketHeaders,
tls: config.TLS,
tlsConfig: tlsConfig,
}

View File

@ -5,6 +5,7 @@ import (
"fmt"
"io"
"net"
"net/http"
"net/url"
"strings"
"time"
@ -21,6 +22,7 @@ type websocketConn struct {
type websocketConfig struct {
host string
path string
headers map[string]string
tls bool
tlsConfig *tls.Config
}
@ -127,7 +129,14 @@ func newWebsocketConn(conn net.Conn, c *websocketConfig) (net.Conn, error) {
Path: c.path,
}
wsConn, resp, err := dialer.Dial(uri.String(), nil)
headers := http.Header{}
if c.headers != nil {
for k, v := range c.headers {
headers.Set(k, v)
}
}
wsConn, resp, err := dialer.Dial(uri.String(), headers)
if err != nil {
var reason string
if resp != nil {

View File

@ -6,6 +6,7 @@ import (
"net"
"net/url"
"os"
"path/filepath"
"strings"
adapters "github.com/Dreamacro/clash/adapters/outbound"
@ -27,8 +28,9 @@ type General struct {
AllowLan bool `json:"allow-lan"`
Mode T.Mode `json:"mode"`
LogLevel log.LogLevel `json:"log-level"`
ExternalController string `json:"external-controller,omitempty"`
Secret string `json:"secret,omitempty"`
ExternalController string
ExternalUI string
Secret string
}
// DNS config
@ -66,9 +68,10 @@ type rawConfig struct {
Mode T.Mode `yaml:"mode"`
LogLevel log.LogLevel `yaml:"log-level"`
ExternalController string `yaml:"external-controller"`
ExternalUI string `yaml:"external-ui"`
Secret string `yaml:"secret"`
DNS *rawDNS `yaml:"dns"`
DNS rawDNS `yaml:"dns"`
Proxy []map[string]interface{} `yaml:"Proxy"`
ProxyGroup []map[string]interface{} `yaml:"Proxy Group"`
Rule []string `yaml:"Rule"`
@ -95,7 +98,7 @@ func readConfig(path string) (*rawConfig, error) {
Rule: []string{},
Proxy: []map[string]interface{}{},
ProxyGroup: []map[string]interface{}{},
DNS: &rawDNS{
DNS: rawDNS{
Enable: false,
},
}
@ -145,10 +148,19 @@ func parseGeneral(cfg *rawConfig) (*General, error) {
redirPort := cfg.RedirPort
allowLan := cfg.AllowLan
externalController := cfg.ExternalController
externalUI := cfg.ExternalUI
secret := cfg.Secret
mode := cfg.Mode
logLevel := cfg.LogLevel
if !filepath.IsAbs(externalUI) {
externalUI = filepath.Join(C.Path.HomeDir(), externalUI)
}
if _, err := os.Stat(externalUI); os.IsNotExist(err) {
return nil, fmt.Errorf("external-ui: %s not exist", externalUI)
}
general := &General{
Port: port,
SocksPort: socksPort,
@ -157,6 +169,7 @@ func parseGeneral(cfg *rawConfig) (*General, error) {
Mode: mode,
LogLevel: logLevel,
ExternalController: externalController,
ExternalUI: externalUI,
Secret: secret,
}
return general, nil
@ -353,7 +366,6 @@ func hostWithDefaultPort(host string, defPort string) (string, error) {
func parseNameServer(servers []string) ([]dns.NameServer, error) {
nameservers := []dns.NameServer{}
log.Debugln("%#v", servers)
for idx, server := range servers {
// parse without scheme .e.g 8.8.8.8:53
@ -387,7 +399,7 @@ func parseNameServer(servers []string) ([]dns.NameServer, error) {
return nameservers, nil
}
func parseDNS(cfg *rawDNS) (*DNS, error) {
func parseDNS(cfg rawDNS) (*DNS, error) {
if cfg.Enable && len(cfg.NameServer) == 0 {
return nil, fmt.Errorf("If DNS configuration is turned on, NameServer cannot be empty")
}

View File

@ -244,8 +244,6 @@ func New(config Config) *Resolver {
mmdb, _ = geoip2.Open(C.Path.MMDB())
})
println(config.EnhancedMode)
r := &Resolver{
main: transform(config.Main),
ipv6: config.IPv6,

View File

@ -27,7 +27,6 @@ func ApplyConfig(cfg *config.Config, force bool) {
}
updateProxies(cfg.Proxies)
updateRules(cfg.Rules)
updateGeneral(cfg.General)
updateDNS(cfg.DNS)
}

View File

@ -12,6 +12,10 @@ func Parse() error {
return err
}
if cfg.General.ExternalUI != "" {
route.SetUIPath(cfg.General.ExternalUI)
}
if cfg.General.ExternalController != "" {
go route.Start(cfg.General.ExternalController, cfg.General.Secret)
}

View File

@ -32,7 +32,7 @@ type configSchema struct {
func getConfigs(w http.ResponseWriter, r *http.Request) {
general := executor.GetGeneral()
render.Respond(w, r, general)
render.JSON(w, r, general)
}
func pointerOrDefault(p *int, def int) int {
@ -46,8 +46,8 @@ func pointerOrDefault(p *int, def int) int {
func patchConfigs(w http.ResponseWriter, r *http.Request) {
general := &configSchema{}
if err := render.DecodeJSON(r.Body, general); err != nil {
w.WriteHeader(http.StatusBadRequest)
render.Respond(w, r, ErrBadRequest)
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, ErrBadRequest)
return
}
@ -68,7 +68,7 @@ func patchConfigs(w http.ResponseWriter, r *http.Request) {
log.SetLevel(*general.LogLevel)
}
w.WriteHeader(http.StatusNoContent)
render.NoContent(w, r)
}
type updateConfigRequest struct {
@ -78,25 +78,25 @@ type updateConfigRequest struct {
func updateConfigs(w http.ResponseWriter, r *http.Request) {
req := updateConfigRequest{}
if err := render.DecodeJSON(r.Body, &req); err != nil {
w.WriteHeader(http.StatusBadRequest)
render.Respond(w, r, ErrBadRequest)
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, ErrBadRequest)
return
}
if !filepath.IsAbs(req.Path) {
w.WriteHeader(http.StatusBadRequest)
render.Respond(w, r, newError("path is not a absoluted path"))
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, newError("path is not a absoluted path"))
return
}
force := r.URL.Query().Get("force") == "true"
cfg, err := executor.ParseWithPath(req.Path)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
render.Respond(w, r, newError(err.Error()))
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, newError(err.Error()))
return
}
executor.ApplyConfig(cfg, force)
w.WriteHeader(http.StatusNoContent)
render.NoContent(w, r)
}

View File

@ -47,8 +47,8 @@ func findProxyByName(next http.Handler) http.Handler {
proxies := T.Instance().Proxies()
proxy, exist := proxies[name]
if !exist {
w.WriteHeader(http.StatusNotFound)
render.Respond(w, r, ErrNotFound)
render.Status(r, http.StatusNotFound)
render.JSON(w, r, ErrNotFound)
return
}
@ -59,14 +59,14 @@ func findProxyByName(next http.Handler) http.Handler {
func getProxies(w http.ResponseWriter, r *http.Request) {
proxies := T.Instance().Proxies()
render.Respond(w, r, map[string]map[string]C.Proxy{
render.JSON(w, r, map[string]map[string]C.Proxy{
"proxies": proxies,
})
}
func getProxy(w http.ResponseWriter, r *http.Request) {
proxy := r.Context().Value(CtxKeyProxy).(C.Proxy)
render.Respond(w, r, proxy)
render.JSON(w, r, proxy)
}
type UpdateProxyRequest struct {
@ -76,8 +76,8 @@ type UpdateProxyRequest struct {
func updateProxy(w http.ResponseWriter, r *http.Request) {
req := UpdateProxyRequest{}
if err := render.DecodeJSON(r.Body, &req); err != nil {
w.WriteHeader(http.StatusBadRequest)
render.Respond(w, r, ErrBadRequest)
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, ErrBadRequest)
return
}
@ -85,18 +85,18 @@ func updateProxy(w http.ResponseWriter, r *http.Request) {
selector, ok := proxy.(*A.Selector)
if !ok {
w.WriteHeader(http.StatusBadRequest)
render.Respond(w, r, ErrBadRequest)
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, ErrBadRequest)
return
}
if err := selector.Set(req.Name); err != nil {
w.WriteHeader(http.StatusBadRequest)
render.Respond(w, r, newError(fmt.Sprintf("Selector update error: %s", err.Error())))
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, newError(fmt.Sprintf("Selector update error: %s", err.Error())))
return
}
w.WriteHeader(http.StatusNoContent)
render.NoContent(w, r)
}
func getProxyDelay(w http.ResponseWriter, r *http.Request) {
@ -104,8 +104,8 @@ func getProxyDelay(w http.ResponseWriter, r *http.Request) {
url := query.Get("url")
timeout, err := strconv.ParseInt(query.Get("timeout"), 10, 16)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
render.Respond(w, r, ErrBadRequest)
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, ErrBadRequest)
return
}
@ -122,14 +122,14 @@ func getProxyDelay(w http.ResponseWriter, r *http.Request) {
select {
case <-time.After(time.Millisecond * time.Duration(timeout)):
w.WriteHeader(http.StatusRequestTimeout)
render.Respond(w, r, ErrRequestTimeout)
render.Status(r, http.StatusRequestTimeout)
render.JSON(w, r, ErrRequestTimeout)
case t := <-sigCh:
if t == 0 {
w.WriteHeader(http.StatusServiceUnavailable)
render.Respond(w, r, newError("An error occurred in the delay test"))
render.Status(r, http.StatusServiceUnavailable)
render.JSON(w, r, newError("An error occurred in the delay test"))
} else {
render.Respond(w, r, map[string]int16{
render.JSON(w, r, map[string]int16{
"delay": t,
})
}

View File

@ -33,8 +33,7 @@ func getRules(w http.ResponseWriter, r *http.Request) {
})
}
w.WriteHeader(http.StatusOK)
render.Respond(w, r, map[string][]Rule{
render.JSON(w, r, map[string][]Rule{
"rules": rules,
})
}

View File

@ -17,6 +17,8 @@ import (
var (
serverSecret = ""
serverAddr = ""
uiPath = ""
)
type Traffic struct {
@ -24,6 +26,10 @@ type Traffic struct {
Down int64 `json:"down"`
}
func SetUIPath(path string) {
uiPath = path
}
func Start(addr string, secret string) {
if serverAddr != "" {
return
@ -49,6 +55,14 @@ func Start(addr string, secret string) {
r.Mount("/proxies", proxyRouter())
r.Mount("/rules", ruleRouter())
if uiPath != "" {
fs := http.StripPrefix("/ui", http.FileServer(http.Dir(uiPath)))
r.Get("/ui", http.RedirectHandler("/ui/", 301).ServeHTTP)
r.Get("/ui/*", func(w http.ResponseWriter, r *http.Request) {
fs.ServeHTTP(w, r)
})
}
log.Infoln("RESTful API listening at: %s", addr)
err := http.ListenAndServe(addr, r)
if err != nil {
@ -77,8 +91,8 @@ func authentication(next http.Handler) http.Handler {
hasUnvalidHeader := text[0] != "Bearer"
hasUnvalidSecret := len(text) == 2 && text[1] != serverSecret
if hasUnvalidHeader || hasUnvalidSecret {
w.WriteHeader(http.StatusUnauthorized)
render.Respond(w, r, ErrUnauthorized)
render.Status(r, http.StatusUnauthorized)
render.JSON(w, r, ErrUnauthorized)
return
}
next.ServeHTTP(w, r)
@ -87,7 +101,7 @@ func authentication(next http.Handler) http.Handler {
}
func traffic(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
render.Status(r, http.StatusOK)
tick := time.NewTicker(time.Second)
t := T.Instance().Traffic()
@ -116,8 +130,8 @@ func getLogs(w http.ResponseWriter, r *http.Request) {
level, ok := log.LogLevelMapping[levelText]
if !ok {
w.WriteHeader(http.StatusBadRequest)
render.Respond(w, r, ErrBadRequest)
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, ErrBadRequest)
return
}

View File

@ -0,0 +1,52 @@
package redir
import (
"errors"
"net"
"syscall"
"unsafe"
"github.com/Dreamacro/go-shadowsocks2/socks"
)
const (
SO_ORIGINAL_DST = 80 // from linux/include/uapi/linux/netfilter_ipv4.h
IP6T_SO_ORIGINAL_DST = 80 // from linux/include/uapi/linux/netfilter_ipv6/ip6_tables.h
)
func parserPacket(conn net.Conn) (socks.Addr, error) {
c, ok := conn.(*net.TCPConn)
if !ok {
return nil, errors.New("only work with TCP connection")
}
rc, err := c.SyscallConn()
if err != nil {
return nil, err
}
var addr socks.Addr
rc.Control(func(fd uintptr) {
addr, err = getorigdst(fd)
})
return addr, err
}
// Call getorigdst() from linux/net/ipv4/netfilter/nf_conntrack_l3proto_ipv4.c
func getorigdst(fd uintptr) (socks.Addr, error) {
raw := syscall.RawSockaddrInet4{}
siz := unsafe.Sizeof(raw)
_, _, err := syscall.Syscall6(syscall.SYS_GETSOCKOPT, fd, syscall.IPPROTO_IP, SO_ORIGINAL_DST, uintptr(unsafe.Pointer(&raw)), uintptr(unsafe.Pointer(&siz)), 0);
if err != 0 {
return nil, err
}
addr := make([]byte, 1+net.IPv4len+2)
addr[0] = socks.AtypIPv4
copy(addr[1:1+net.IPv4len], raw.Addr[:])
port := (*[2]byte)(unsafe.Pointer(&raw.Port)) // big-endian
addr[1+net.IPv4len], addr[1+net.IPv4len+1] = port[0], port[1]
return addr, nil
}

View File

@ -5,6 +5,7 @@ import (
"io"
"net"
"net/http"
"strings"
"sync"
"time"
@ -25,8 +26,13 @@ func (t *Tunnel) handleHTTP(request *adapters.HTTPAdapter, proxy C.ProxyAdapter)
conn := newTrafficTrack(proxy.Conn(), t.traffic)
req := request.R
host := req.Host
keepalive := true
for {
if strings.ToLower(req.Header.Get("Connection")) == "close" {
keepalive = false
}
req.Header.Set("Connection", "close")
req.RequestURI = ""
adapters.RemoveHopByHopHeaders(req.Header)
@ -53,6 +59,10 @@ func (t *Tunnel) handleHTTP(request *adapters.HTTPAdapter, proxy C.ProxyAdapter)
break
}
if !keepalive {
break
}
req, err = http.ReadRequest(bufio.NewReader(request.Conn()))
if err != nil {
break

View File

@ -120,8 +120,6 @@ func (t *Tunnel) handleConn(localConn C.ServerAdapter) {
log.Debugln("[DNS] %s --> %s", metadata.Host, ip.String())
metadata.IP = &ip
}
} else {
log.Debugln("[DNS] unknown%#v", metadata)
}
var proxy C.Proxy