diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..fe97206 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ +# 2023/12/26 +- [x] feat: upgrade to surge like config +- [x] feat: support windows 10 +- [x] tech: merge proxy code +- [x] feat: reload rule by manager: curl http://127.0.0.1:9200/reload + +# plan +- [ ] feat: default hijack dns query +- [ ] feat: show process name of network +- [ ] bug: traffic will be endless loop if proxy's ip use proxy by rule +- [ ] feat: support ss protocol +- [ ] feat: support IPv6 +- [ ] feat: update GEOIP database \ No newline at end of file diff --git a/README.md b/README.md index 4306a05..c6f9e28 100644 --- a/README.md +++ b/README.md @@ -26,16 +26,9 @@ The default web status port is 9200 , just visit http://localhost:9200/ to check ## Documents -* [how to use with Raspberry Pi (在树莓派上使用kone)](./misc/docs/how-to-use-with-raspberry-pi.md) -* [how to use kone in an ENT network (企业网中如何使用kone)](./misc/docs/kone-in-ent-network.md) -* [how to make gotunnel & kone work together(gotunnel和kone的珠联璧合)](./misc/docs/gotunnel-kone-work-together.md) +* [how to use with Raspberry Pi](./misc/docs/how-to-use-with-raspberry-pi.md) +* [how to use kone in an ENT network](./misc/docs/kone-in-ent-network.md) +* [how to make gotunnel & kone work together](./misc/docs/gotunnel-kone-work-together.md) ## License The MIT License (MIT) Copyright (c) 2016 xjdrew - -## todo -- [x] upgrade to surge like config -- [x] support windows 10 -- [ ] merge proxy code -- [ ] default hijack dns query -- [ ] show process name of network diff --git a/cmd/kone/main.go b/cmd/kone/main.go index b5b17ae..9c8cf29 100644 --- a/cmd/kone/main.go +++ b/cmd/kone/main.go @@ -18,18 +18,11 @@ var VERSION = "0.3-dev" var logger = logging.MustGetLogger("kone") -func InitLogger(debug bool) { - format := logging.MustStringFormatter( +func init() { + logging.SetFormatter(logging.MustStringFormatter( `%{color}%{time:06-01-02 15:04:05.000} %{level:.4s} @%{shortfile}%{color:reset} %{message}`, - ) - logging.SetFormatter(format) + )) logging.SetBackend(logging.NewLogBackend(os.Stdout, "", 0)) - - if debug { - logging.SetLevel(logging.DEBUG, "kone") - } else { - logging.SetLevel(logging.INFO, "kone") - } } func main() { @@ -43,7 +36,11 @@ func main() { os.Exit(1) } - InitLogger(*debug) + if *debug { + logging.SetLevel(logging.DEBUG, "kone") + } else { + logging.SetLevel(logging.INFO, "kone") + } configFile := *config if configFile == "" { diff --git a/config.go b/config.go index 462aca3..f2c0348 100644 --- a/config.go +++ b/config.go @@ -13,6 +13,10 @@ import ( "gopkg.in/ini.v1" ) +func init() { + ini.PrettyFormat = true +} + const ( HTTP_PROXY = "http_proxy" HTTPS_PROXY = "https_proxy" @@ -48,6 +52,9 @@ type RuleConfig struct { } type KoneConfig struct { + source interface{} // config source: file name or raw ini data + inif *ini.File // parsed ini file + General GeneralConfig Core CoreConfig Proxy map[string]string @@ -87,8 +94,9 @@ func (cfg *KoneConfig) check() (err error) { return nil } -func ParseConfig(filename interface{}) (*KoneConfig, error) { +func ParseConfig(source interface{}) (*KoneConfig, error) { cfg := new(KoneConfig) + cfg.source = source // set default value cfg.Core.Network = "10.192.0.1/16" @@ -107,11 +115,12 @@ func ParseConfig(filename interface{}) (*KoneConfig, error) { cfg.Core.DnsWriteTimeout = DnsDefaultWriteTimeout // decode config value - f, err := ini.LoadSources(ini.LoadOptions{AllowBooleanKeys: true, KeyValueDelimiters: "="}, filename) + f, err := ini.LoadSources(ini.LoadOptions{AllowBooleanKeys: true, KeyValueDelimiters: "="}, source) if err != nil { logger.Errorf("%v", err) return nil, err } + cfg.inif = f err = f.MapTo(cfg) if err != nil { diff --git a/dns_table.go b/dns_table.go index 4694d3f..132032b 100644 --- a/dns_table.go +++ b/dns_table.go @@ -156,11 +156,21 @@ func (c *DnsTable) clearExpiredNonProxyDomain(now time.Time) { for domain, expired := range c.nonProxyDomains { if expired.Before(now) { delete(c.nonProxyDomains, domain) - logger.Debugf("[dns] release non proxy domain: %s", domain) + logger.Debugf("[dns] release expired non proxy domain: %s", domain) } } } +func (c *DnsTable) ClearNonProxyDomain() { + c.npdLock.Lock() + defer c.npdLock.Unlock() + for domain := range c.nonProxyDomains { + delete(c.nonProxyDomains, domain) + logger.Debugf("[dns] release non proxy domain: %s", domain) + + } +} + func (c *DnsTable) clearExpiredDomain(now time.Time) { c.recordsLock.Lock() defer c.recordsLock.Unlock() @@ -189,6 +199,7 @@ func (c *DnsTable) Serve() error { tick := time.NewTicker(60 * time.Second) for now := range tick.C { c.clearExpiredDomain(now) + //TODO: is it necessary? c.clearExpiredNonProxyDomain(now) } return nil diff --git a/manager.go b/manager.go index 32059e0..a597b72 100644 --- a/manager.go +++ b/manager.go @@ -183,6 +183,24 @@ table th.title { {{template "footer" .}} {{end}} + + +{{define "config"}} +{{template "header" .}} +

Config

+ + + + + + + + +
Source
{{.Source}}
+{{template "footer" .}} +{{end}} ` // statistical data of every connection @@ -212,6 +230,7 @@ type TrafficRecord struct { type Manager struct { one *One + cfg *KoneConfig startTime time.Time // process start time listen string tmpl *template.Template @@ -255,6 +274,8 @@ func (m *Manager) indexHandle(w io.Writer, r *http.Request) error { "/website/", "/proxy/", "/dns/", + "/reload/", + "/config/", }, }) } @@ -343,6 +364,34 @@ func (m *Manager) dnsHandle(w io.Writer, r *http.Request) error { }) } +func (m *Manager) reloadHandle(w io.Writer, r *http.Request) error { + logger.Infof("[manager] reload config") + newcfg, err := ParseConfig(m.cfg.source) + if err != nil { + return err + } + + err = m.one.Reload(newcfg) + if err != nil { + return err + } + + m.cfg = newcfg + w.Write([]byte("reload succeed")) + return nil +} + +func (m *Manager) configHandle(w io.Writer, r *http.Request) error { + b := bytes.NewBuffer([]byte{}) + m.cfg.inif.WriteTo(b) + + return m.tmpl.ExecuteTemplate(w, "config", map[string]interface{}{ + "Title": "Config", + "RuleCount": len(m.cfg.Rule), + "Source": string(b.Bytes()), + }) +} + // statistical data api func (m *Manager) consumeData() { accumulate := func(s map[string]*TrafficRecord, name string, endpoint string, upload int64, download int64, now time.Time) { @@ -394,14 +443,16 @@ func (m *Manager) Serve() error { http.HandleFunc("/website/", handleWrapper(m.websiteHandle)) http.HandleFunc("/proxy/", handleWrapper(m.proxyHandle)) http.HandleFunc("/dns/", handleWrapper(m.dnsHandle)) + http.HandleFunc("/reload/", handleWrapper(m.reloadHandle)) + http.HandleFunc("/config/", handleWrapper(m.configHandle)) go m.consumeData() logger.Infof("[manager] listen on: %s", m.listen) return http.ListenAndServe(m.listen, nil) } -func NewManager(one *One, cfg GeneralConfig) *Manager { - if cfg.ManagerAddr == "" { +func NewManager(one *One, cfg *KoneConfig) *Manager { + if cfg.General.ManagerAddr == "" { return nil } @@ -441,8 +492,9 @@ func NewManager(one *One, cfg GeneralConfig) *Manager { return &Manager{ one: one, + cfg: cfg, startTime: time.Now(), - listen: cfg.ManagerAddr, + listen: cfg.General.ManagerAddr, dataCh: make(chan ConnData), hosts: make(map[string]*TrafficRecord), websites: make(map[string]*TrafficRecord), diff --git a/misc/example/example.ini b/misc/example/example.ini deleted file mode 100644 index 3699337..0000000 --- a/misc/example/example.ini +++ /dev/null @@ -1,239 +0,0 @@ -[general] -# outbound network interface -#out = eth0 - -# virtual network -# tun name, auto allocate if not set -# DEFAULT VALUE: "" -# tun = tun0 - -# inet addr/mask -# DEFAULT VALUE: 10.192.0.1/16 -# if a domain matches a pattern and the pattern uses proxy -# the domain will be hijacked to the virtual network -network = 10.192.0.1/16 - -# nat config -[tcp] -#listen-port = 82 -#nat-port-start = 10000 -#nat-port-end = 60000 - -[udp] -#listen-port = 82 -#nat-port-start = 10000 -#nat-port-end = 60000 - -[dns] -# DEFAULT VALUE: 53 -# dns-port = 53 - -# backend dns -# DEFAULT VALUE: 114.114.114.114, 223.5.5.5 -# nameserver = 114.114.114.114 -# nameserver = 223.5.5.5 - -# dns-ttl = 600 -# dns-packet-size = 4096 -# dns-read-timeout = 5 -# dns-write-timeout = 5 - -# set tun0 as default gate for this ip -[route] -# telegram -v = 91.108.0.0/16 -v = 149.154.0.0/16 - -# define a proxy named "A" -[proxy "A"] -url = http://example.com:3228 -default = yes - -# define a proxy named "B" -# [proxy "B"] -# url = socks5://example.com:2080 - -# define a pattern and outbound proxy -# if don't set proxy, packets will be sent to target directly -[pattern "direct-website"] -scheme = DOMAIN-SUFFIX -v = cn -v = 126.net -v = 163.com -v = appldnld.apple.com -v = adcdownload.apple.com -v = alicdn.com -v = amap.com -v = bdimg.com -v = bdstatic.com -v = cnbeta.com -v = cnzz.com -v = douban.com -v = gtimg.com -v = hao123.com -v = haosou.com -v = icloud-content.com -v = ifeng.com -v = iqiyi.com -v = jd.com -v = lcdn-registration.apple.com -v = ls.apple.com -v = netease.com -v = phobos.apple.com -v = qhimg.com -v = qq.com -v = sogou.com -v = sohu.com -v = soso.com -v = suning.com -v = swcdn.apple.com -v = tmall.com -v = tudou.com -v = weibo.com -v = xunlei.com -v = youku.com -v = zhihu.com - -[pattern "direct-website-keyword"] -scheme = DOMAIN-KEYWORD -v = 360buy -v = alipay -v = baidu -v = qiyi -v = sohu -v = taobao - -[pattern "proxy-website"] -proxy = A -scheme = DOMAIN-SUFFIX -v = appspot.com -v = t.co,Proxy -v = twimg.com -v = amazonaws.com -v = android.com -v = angularjs.org -v = akamaihd.net -v = bit.ly -v = bitbucket.org -v = blog.com -v = blogcdn.com -v = blogger.com -v = blogsmithmedia.com -v = box.net -v = bloomberg.com -v = chromium.org -v = cl.ly -v = cloudfront.net -v = cloudflare.com -v = cocoapods.org -v = crashlytics.com -v = dribbble.com -v = dropbox.com -v = dropboxstatic.com -v = dropboxusercontent.com -v = docker.com -v = duckduckgo.com -v = digicert.com -v = dnsimple.com -v = edgecastcdn.net -v = engadget.com -v = eurekavpt.com -v = fb.me -v = fbcdn.net -v = fc2.com -v = feedburner.com -v = fabric.io -v = flickr.com -v = fastly.net -v = ggpht.com -v = github.com -v = github.io -v = githubusercontent.com -v = golang.org -v = goo.gl -v = gstatic.com -v = godaddy.com -v = gravatar.com -v = imageshack.us -v = imgur.com -v = jshint.com -v = ift.tt -v = itunes.com -v = j.mp -v = kat.cr -v = linode.com -v = linkedin.com -v = licdn.com -v = lithium.com -v = megaupload.com -v = mobile01.com -v = modmyi.com -v = mzstatic.com -v = nytimes.com -v = name.com -v = openvpn.net -v = openwrt.org -v = ow.ly -v = pinboard.in -v = ssl-images-amazon.com -v = sstatic.net -v = stackoverflow.com -v = staticflickr.com -v = squarespace.com -v = symcd.com -v = symcb.com -v = symauth.com -v = ubnt.com -v = thepiratebay.org -v = tumblr.com -v = twitch.tv -v = wikipedia.com -v = wikipedia.org -v = wikimedia.org -v = wordpress.com -v = wsj.com -v = wsj.net -v = wp.com -v = vimeo.com -v = youtu.be -v = ytimg.com - -[pattern "proxy-website-keyword"] -proxy = A -scheme = DOMAIN-KEYWORD -v = google -v = gmail -v = facebook -v = instagram -v = twitter -v = youtube -v = blogspot - -[pattern "internal-ip"] -scheme = IP-CIDR -v = 10.0.0.0/8 -v = 127.0.0.1/8 -v = 172.16.0.0/16 -v = 192.168.0.0/16 - -[pattern "direct-country"] -scheme = IP-COUNTRY -v = CN - -[pattern "proxy-country"] -proxy = A -scheme = IP-COUNTRY -v = US -v = HK - -# rules define the order of checking pattern -[rule] -pattern = direct-website -pattern = direct-website-keyword -pattern = proxy-website -pattern = proxy-website-keyword -pattern = internal-ip -pattern = direct-country -# set to a proxy for domaines that don't match any pattern -# DEFAULT VALUE: "" -final = A diff --git a/one.go b/one.go index cbcbbd2..36489bb 100644 --- a/one.go +++ b/one.go @@ -55,6 +55,12 @@ func (one *One) Serve() { wg.Wait() } +func (one *One) Reload(cfg *KoneConfig) error { + one.rule = NewRule(cfg.Rule) + one.dnsTable.ClearNonProxyDomain() + return nil +} + func FromConfig(cfg *KoneConfig) (*One, error) { ip, subnet, _ := net.ParseCIDR(cfg.Core.Network) @@ -104,6 +110,6 @@ func FromConfig(cfg *KoneConfig) (*One, error) { } // new manager - one.manager = NewManager(one, cfg.General) + one.manager = NewManager(one, cfg) return one, nil } diff --git a/proxy/proxy.go b/proxy/proxy.go index e8be54b..115afa8 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -9,12 +9,8 @@ import ( "errors" "net" "net/url" - - "github.com/op/go-logging" ) -var logger = logging.MustGetLogger("kone") - // A Dialer is a means to establish a connection. type Dialer interface { // Dial connects to the given address via the proxy. @@ -29,7 +25,6 @@ var proxySchemes = make(map[string]func(*url.URL, Dialer) (Dialer, error)) // a URL with that scheme and a forwarding Dialer. Registered schemes are used // by FromURL. func registerDialerType(scheme string, f func(*url.URL, Dialer) (Dialer, error)) { - logger.Debugf("register proxy schema %s", scheme) proxySchemes[scheme] = f } diff --git a/tcp_relay.go b/tcp_relay.go index 98877d9..0f868fd 100644 --- a/tcp_relay.go +++ b/tcp_relay.go @@ -34,7 +34,7 @@ func (r *TCPRelay) realRemoteHost(conn net.Conn, connData *ConnData) (addr strin session := r.nat.getSession(remotePort) if session == nil { - logger.Errorf("[tcp] %s > %s no session", conn.LocalAddr(), remoteAddr) + logger.Errorf("[tcp relay] %s > %s no session", conn.LocalAddr(), remoteAddr) return } @@ -45,7 +45,7 @@ func (r *TCPRelay) realRemoteHost(conn net.Conn, connData *ConnData) (addr strin if one.dnsTable.IsLocalIP(dstIP) { // for dns hijacked traffic record := one.dnsTable.GetByIP(dstIP) if record == nil { - logger.Debugf("[tcp] %s:%d > %s:%d dns expired", session.srcIP, session.srcPort, dstIP, session.dstPort) + logger.Debugf("[tcp relay] %s:%d > %s:%d dns expired", session.srcIP, session.srcPort, dstIP, session.dstPort) return } @@ -61,7 +61,7 @@ func (r *TCPRelay) realRemoteHost(conn net.Conn, connData *ConnData) (addr strin connData.Proxy = proxy addr = fmt.Sprintf("%s:%d", host, session.dstPort) - logger.Debugf("[tcp] tunnel %s:%d > %s proxy %q", session.srcIP, session.srcPort, addr, proxy) + logger.Debugf("[tcp relay] tunnel %s:%d > %s proxy %q", session.srcIP, session.srcPort, addr, proxy) return } @@ -108,19 +108,20 @@ func (r *TCPRelay) Serve() error { addr := &net.TCPAddr{IP: r.relayIP, Port: int(r.relayPort)} ln, err := net.ListenTCP("tcp", addr) if err != nil { + logger.Errorf("[tcp relay] listen failed: %v", err) return err } - logger.Infof("[tcp] listen on %v", addr) + logger.Infof("[tcp relay] listen on %v", addr) for { conn, err := ln.AcceptTCP() if err != nil { - logger.Errorf("acceept failed temporary: %v", err) + logger.Errorf("[tcp relay] acceept failed temporary: %v", err) time.Sleep(time.Second) //prevent log storms continue } - logger.Debugf("[tcp] new connection [%s > %s]", conn.RemoteAddr(), conn.LocalAddr()) + logger.Debugf("[tcp relay] new connection [%s > %s]", conn.RemoteAddr(), conn.LocalAddr()) go r.handleConn(conn) } } diff --git a/tun.go b/tun.go index a5c22c0..34c0126 100644 --- a/tun.go +++ b/tun.go @@ -28,6 +28,7 @@ func (tun *TunDriver) Serve() error { for { n, err := ifce.Read(buffer) if err != nil { + logger.Errorf("[tun] read failed: %v", err) return err } diff --git a/udp_relay.go b/udp_relay.go index 08ea7f6..ee5529b 100644 --- a/udp_relay.go +++ b/udp_relay.go @@ -77,7 +77,7 @@ func (r *UDPRelay) grabTunnel(localConn *net.UDPConn, cliaddr *net.UDPAddr) *UDP srvaddr := &net.UDPAddr{IP: record.RealIP, Port: int(session.dstPort)} remoteConn, err := net.DialUDP("udp", nil, srvaddr) if err != nil { - logger.Errorf("[udp] connect to %s failed: %v", srvaddr, err) + logger.Errorf("[udp relay] connect to %s failed: %v", srvaddr, err) return nil } tunnel = &UDPTunnel{ @@ -88,16 +88,16 @@ func (r *UDPRelay) grabTunnel(localConn *net.UDPConn, cliaddr *net.UDPAddr) *UDP remoteConn: remoteConn, } - logger.Debugf("[udp] %s:%d > %v: new tunnel", session.srcIP, session.srcPort, srvaddr) + logger.Debugf("[udp relay] %s:%d > %v: new tunnel", session.srcIP, session.srcPort, srvaddr) r.tunnels[addr] = tunnel go func() { err := tunnel.Pump() if err != nil { - logger.Debugf("[udp] pump to %v failed: %v", tunnel.remoteConn.RemoteAddr(), err) + logger.Debugf("[udp relay] pump to %v failed: %v", tunnel.remoteConn.RemoteAddr(), err) } tunnel.remoteConn.Close() - logger.Debugf("[udp] %s:%d > %v: destroy tunnel", tunnel.session.srcIP, tunnel.session.srcPort, srvaddr) + logger.Debugf("[udp relay] %s:%d > %v: destroy tunnel", tunnel.session.srcIP, tunnel.session.srcPort, srvaddr) r.lock.Lock() delete(r.tunnels, addr) @@ -111,12 +111,12 @@ func (r *UDPRelay) grabTunnel(localConn *net.UDPConn, cliaddr *net.UDPAddr) *UDP func (r *UDPRelay) handlePacket(localConn *net.UDPConn, cliaddr *net.UDPAddr, packet []byte) { tunnel := r.grabTunnel(localConn, cliaddr) if tunnel == nil { - logger.Errorf("[udp] %v > %v: grap tunnel failed", cliaddr, localConn.LocalAddr()) + logger.Errorf("[udp relay %v > %v: grap tunnel failed", cliaddr, localConn.LocalAddr()) return } _, err := tunnel.Write(packet) if err != nil { - logger.Debugf("[udp] %v", err) + logger.Debugf("[udp relay] tunnel write failed: %v", err) } } @@ -124,6 +124,7 @@ func (r *UDPRelay) Serve() error { addr := &net.UDPAddr{IP: r.relayIP, Port: int(r.relayPort)} conn, err := net.ListenUDP("udp", addr) if err != nil { + logger.Errorf("[udp relay] listen failed: %v", err) return err } @@ -131,7 +132,9 @@ func (r *UDPRelay) Serve() error { b := make([]byte, MTU) n, cliaddr, err := conn.ReadFromUDP(b) if err != nil { - return err + logger.Errorf("[udp relay] acceept failed temporary: %v", err) + time.Sleep(time.Second) //prevent log storms + continue } go r.handlePacket(conn, cliaddr, b[:n]) }