├── .gitignore ├── .gitmodules ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── flora.default.conf ├── flora ├── config.go ├── flora.go ├── geoip.go ├── http.go ├── network_setup.go ├── network_setup_mac.go ├── network_setup_win.go ├── proxy_direct.go ├── proxy_reject.go ├── proxy_shadowsocks.go ├── socks4.go └── socks5.go ├── geoip.mmdb ├── geoip_test.go ├── goreleaser.yml └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | *.user.conf 22 | 23 | *.exe 24 | *.test 25 | *.prof 26 | *.zip 27 | flora-kit 28 | .DS_Store 29 | *.log 30 | release/ 31 | *.tar.* 32 | .idea 33 | dist/ 34 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/shadowsocks-go"] 2 | path = vendor/shadowsocks-go 3 | url = https://github.com/shadowsocks/shadowsocks-go.git 4 | [submodule "vendor/ini"] 5 | path = vendor/ini 6 | url = https://github.com/go-ini/ini.git 7 | [submodule "vendor/geoip2-golang"] 8 | path = vendor/geoip2-golang 9 | url = https://github.com/oschwald/geoip2-golang.git 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 0.2.3 2 | ----- 3 | - 支持```[General]skip-Proxy``` 4 | - 重构版,方便其他代理模块加入 5 | 6 | 0.2.2 7 | ----- 8 | 9 | - 修正打包确缺失默认配置文件的问题。 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | RELEASE_PATH = release 2 | PACKAGE_PATH = release/flora 3 | 4 | install: 5 | @go get 6 | build: 7 | GOOS=darwin GOARCH=amd64 go build -ldflags "-s -w" -o $(RELEASE_PATH)/flora-darwin-amd64 8 | GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o $(RELEASE_PATH)/flora-amd64 9 | GOOS=linux GOARCH=386 go build -ldflags "-s -w" -o $(RELEASE_PATH)/flora-386 10 | GOOS=windows GOARCH=386 go build -ldflags "-s -w" -o $(RELEASE_PATH)/flora-386.exe 11 | GOOS=windows GOARCH=amd64 go build -ldflags "-s -w" -o $(RELEASE_PATH)/flora-amd64.exe 12 | package: 13 | rm -Rf $(PACKAGE_PATH)/* 14 | mkdir -p $(PACKAGE_PATH) 15 | cp ./flora.default.conf $(PACKAGE_PATH) 16 | cp ./geoip.mmdb $(PACKAGE_PATH) 17 | cp ./LICENSE $(RELEASE_PATH) 18 | cp ./README.md $(PACKAGE_PATH) 19 | # macOS 20 | cp ./release/flora-darwin-amd64 $(PACKAGE_PATH) 21 | cd ./release && zip flora-darwin-amd64.zip flora 22 | # Linux amd64 23 | cp ./release/flora-amd64 $(PACKAGE_PATH)flora 24 | cd ./release && tar zcf flora-linux-amd64.tar.gz flora 25 | # Linux 386 26 | cp ./release/flora-386 $(PACKAGE_PATH) 27 | cd ./release && tar zcf flora-linux-386.tar.gz flora 28 | # Windows 386 29 | #cp ./release/flora-386.exe $(PACKAGE_PATH) 30 | #cd ./release && tar zcf flora-win-386.tar.gz flora 31 | # Windows amd64 32 | #cp ./release/flora-amd64.exe $(PACKAGE_PATH) 33 | #cd ./release && tar zcf flora-win-amd64.tar.gz flora 34 | # remove history 35 | rm $(PACKAGE_PATH)flora 36 | run: 37 | @go run main.go 38 | test: 39 | @go test ./flora 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Flora 2 | ----- 3 | 4 | 基于 [shadowsocks-go](https://github.com/shadowsocks/shadowsocks-go) 做的完善实现,完全兼容 Surge 的配置文件。 5 | 6 | > NOTE: 目前已完整实现自动 Proxy 的逻辑,可以用了,已在自己的 macOS 环境连续跑了两天,稳定有效。 7 | 8 | 2016-11-22 11 00 00 9 | 10 | ## 功能列表 11 | 12 | - macOS 和 Linux 同时支持; 13 | - 连接 ShadowSocks 代理,并在本地建立 socks 代理服务,以提供给系统代理配置使用; 14 | - 支持域名关键词、前缀、后缀匹配,制定 Direct 访问(白名单)或用 Proxy 访问(黑名单); 15 | - 支持 IP 白名单,黑名单; 16 | - 支持 GeoIP 判断目标网站服务器所在区域,自动选择线路; 17 | - 启动的时候自动改变 macOS,windows 网路代理配置,无需手工调整; 18 | 19 | 20 | ## TODO 21 | 22 | - HTTP, HTTPS proxy 实现; 23 | - 自动代理 pac 实现; 24 | - 支持 Linux 网络代理自动设置; 25 | 26 | ## 下载 && 运行 27 | 28 | https://github.com/huacnlee/flora-kit/releases 29 | 30 | 请根据系统下载需要的 release 包。 31 | 32 | > NOTE: 由于启动的时候,需要修改系统的网络配置,所以你需要用 sudo 来执行: 33 | 34 | #### macOS 35 | ``` 36 | $ cd flora 37 | $ sudo ./flora 38 | ``` 39 | 40 | #### Linux 41 | ``` 42 | $ cd flora 43 | $ ./flora 44 | ``` 45 | 46 | #### Windows 47 | ``` 48 | flora.exe 49 | ``` 50 | 51 | #### 开发说明 52 | 由于 go get 经常下不了包,我把依赖的几个库用vendor的方式加入工程 53 | 开发的时候需要执行下面的操作: 54 | ``` 55 | git submodule init 56 | git submodule update 57 | ``` 58 | 59 | ## License 60 | 61 | Apache License 2.0 62 | -------------------------------------------------------------------------------- /flora.default.conf: -------------------------------------------------------------------------------- 1 | [General] 2 | loglevel = notify 3 | replica = false 4 | skip-Proxy = 127.0.0.1, 192.168.0.0/16, 10.0.0.0/8, 172.16.0.0/12, 100.64.0.0/10, localhost, *.local, e.crashlytics.com 5 | 6 | # 以下参数仅供 iOS 版本使用 7 | bypass-system = true 8 | bypass-tun = 192.168.0.0/16, 10.0.0.0/8, 172.16.0.0/12, 0.0.0.0/31 9 | ipv6 = true 10 | 11 | # 以下参数仅供 macOS 版本使用 12 | interface = 127.0.0.1 13 | socks-port = 1080 14 | 15 | allow-wifi-access = true 16 | enhanced-mode-by-rule = true 17 | exclude-simple-hostnames = true 18 | 19 | [Proxy] 20 | DIRECT = direct 21 | # Proxy = shadowsocks, server-ip, port, method, password 22 | 23 | [Proxy Group] 24 | # 🚀 Proxy = select, 🌞 Line 25 | 26 | [Rule] 27 | // BLOCK ADS 28 | # DOMAIN-SUFFIX,mgid.com,REJECT 29 | // DIRECT RULES 30 | # DOMAIN-SUFFIX,cn,DIRECT 31 | // NORMAL RULES 32 | # DOMAIN-KEYWORD,instagram,Proxy 33 | # DOMAIN-SUFFIX,youtube.com,Proxy 34 | // China IP use DIRECT 35 | GEOIP,CN,DIRECT 36 | # Other 37 | FINAL,Proxy 38 | -------------------------------------------------------------------------------- /flora/config.go: -------------------------------------------------------------------------------- 1 | package flora 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net" 7 | "os" 8 | "strconv" 9 | "strings" 10 | 11 | "ini" 12 | ss "shadowsocks-go/shadowsocks" 13 | "math/rand" 14 | ) 15 | 16 | const ( 17 | DEFAULT_SOCKS_PORT = 1080 18 | ) 19 | 20 | type ProxyConfig struct { 21 | SurgeConfig *ini.File 22 | GeoDbPath string 23 | LocalSocksPort int 24 | LocalHost string 25 | proxyServer map[string]ProxyServer 26 | proxyGroup map[string]*proxyGroup 27 | 28 | bypassDomains []interface{} 29 | systemBypass []string 30 | ruleSuffixDomains []*Rule 31 | rulePrefixDomains []*Rule 32 | ruleKeywordDomains []*Rule 33 | ruleUserAgent []*Rule 34 | ruleGeoIP []*Rule 35 | ruleFinal *Rule 36 | } 37 | type proxyGroup struct { 38 | mode string 39 | proxyServers []ProxyServer 40 | } 41 | 42 | func LoadConfig(cfgFile string, geoFile string) *ProxyConfig { 43 | proxyConfig := ProxyConfig{} 44 | var iniOpts = ini.LoadOptions{ 45 | AllowBooleanKeys: true, 46 | Loose: true, 47 | Insensitive: true, 48 | } 49 | sep := string(os.PathSeparator) 50 | pwd, _ := os.Getwd() 51 | var geoFilename = geoFile 52 | var err error 53 | var defaultCfgName = strings.Join([]string{pwd, "flora.default.conf"}, sep) 54 | var userConfigFilename = strings.Join([]string{pwd, "flora.user.conf"}, sep) 55 | var speCfgName string 56 | if _, err := os.Stat(cfgFile); nil != err && os.IsNotExist(err) { 57 | speCfgName = strings.Join([]string{pwd, cfgFile}, sep) 58 | } 59 | if _, err := os.Stat(geoFilename); nil != err && os.IsNotExist(err) { 60 | geoFilename = strings.Join([]string{pwd, "geoip.mmdb"}, sep) 61 | } 62 | 63 | proxyConfig.SurgeConfig, err = ini.LoadSources(iniOpts, speCfgName, defaultCfgName, userConfigFilename) 64 | 65 | if err != nil { 66 | panic(fmt.Sprintf("Config file %v not found, or have error: \n\t%v", cfgFile, err)) 67 | } 68 | loadGeneral(&proxyConfig) 69 | loadProxy(&proxyConfig) 70 | loadProxyGroup(&proxyConfig) 71 | loadGeoIP(geoFilename) 72 | loadRules(&proxyConfig) 73 | 74 | return &proxyConfig 75 | } 76 | 77 | // [General] section 78 | func loadGeneral(config *ProxyConfig) { 79 | section := config.SurgeConfig.Section("General") 80 | bypassDomains := []string{} 81 | if section.HasKey("skip-proxy") { 82 | bypassDomains = append(bypassDomains, readArrayLine(section.Key("skip-proxy").String())...) 83 | } 84 | if section.HasKey("bypass-tun") { 85 | bypassDomains = append(bypassDomains, readArrayLine(section.Key("bypass-tun").String())...) 86 | } 87 | config.LocalSocksPort = DEFAULT_SOCKS_PORT 88 | if section.HasKey("socks-port") { 89 | port, err := strconv.Atoi(section.Key("socks-port").String()) 90 | if nil == err { 91 | config.LocalSocksPort = port 92 | } 93 | } 94 | config.LocalHost = "127.0.0.1" 95 | if section.Haskey("interface") { 96 | ipStr := section.Key("interface").String() 97 | addr := net.ParseIP(ipStr) 98 | if nil != addr { 99 | config.LocalHost = ipStr 100 | } 101 | } 102 | config.systemBypass = bypassDomains 103 | //load bypass 104 | config.bypassDomains = make([]interface{}, len(bypassDomains)) 105 | for i, v := range bypassDomains { 106 | ip := net.ParseIP(v) 107 | if nil != ip { 108 | config.bypassDomains[i] = ip 109 | } else if _, n, err := net.ParseCIDR(v); err == nil { 110 | config.bypassDomains[i] = n 111 | } else { 112 | config.bypassDomains[i] = v 113 | } 114 | } 115 | 116 | } 117 | 118 | // [Proxy] Section 119 | func loadProxy(config *ProxyConfig) { 120 | config.proxyServer = make(map[string]ProxyServer) 121 | section := config.SurgeConfig.Section("Proxy") 122 | for _, name := range section.KeyStrings() { 123 | proxyName := strings.ToLower(name) 124 | v, _ := section.GetKey(proxyName) 125 | var proxyStrCfg = readArrayLine(v.String()) 126 | serverType := strings.ToLower(proxyStrCfg[0]) 127 | var proxy ProxyServer 128 | if serverType == ServerTypeShadowSocks || serverType == ServerTypeCustom { 129 | //[ip:port,password,method] 130 | if len(proxyStrCfg) > 1 { 131 | c, err := ss.NewCipher(proxyStrCfg[3], proxyStrCfg[4]) 132 | if nil != err { 133 | log.Printf("Loading shadowsocks proxy server %s has error ", proxyName) 134 | continue 135 | } 136 | proxy = NewShadowSocks(strings.Join(proxyStrCfg[1:3], ":"), c) 137 | } 138 | 139 | } else if serverType == ServerTypeDirect { 140 | proxy = NewDirect() 141 | } else if serverType == ServerTypeReject { 142 | proxy = NewReject() 143 | } 144 | if nil != proxy { 145 | log.Printf("Loading proxy server %s done. ", proxyName) 146 | config.proxyServer[proxyName] = proxy 147 | } 148 | } 149 | 150 | } 151 | 152 | func (c *ProxyConfig) GetProxyServer(action string) (ProxyServer,error) { 153 | const maxFailCnt = 30 154 | 155 | a := strings.ToLower(action) 156 | 157 | if server, ok := c.proxyServer[a]; ok { 158 | return server,nil 159 | } 160 | 161 | if group, ok := c.proxyGroup[a]; ok { 162 | for _, s := range group.proxyServers { 163 | eff := make([]ProxyServer,0) 164 | if s.FailCount() < maxFailCnt { 165 | eff = append(eff, s) 166 | } 167 | l := len(eff) 168 | if l > 0 { 169 | return eff[rand.Intn(l)],nil 170 | } 171 | } 172 | } 173 | return nil,errProxy 174 | } 175 | 176 | //[Proxy Group] Section 177 | func loadProxyGroup(config *ProxyConfig) { 178 | section := config.SurgeConfig.Section("Proxy Group") 179 | config.proxyGroup = make(map[string]*proxyGroup) 180 | for _, name := range section.KeyStrings() { 181 | groupName := strings.ToLower(name) 182 | v, _ := section.GetKey(groupName) 183 | proxyArr := readArrayLine(v.String()) 184 | //🚀 Proxy = select, 🌞 Line 185 | if len(proxyArr) > 1 { 186 | groupItems := proxyGroup{mode: proxyArr[0]} 187 | servers := make([]ProxyServer, len(proxyArr)-1) 188 | for i, p := range proxyArr[1:] { 189 | proxyName := strings.ToLower(p) 190 | servers[i] = config.proxyServer[proxyName] 191 | } 192 | groupItems.proxyServers = servers 193 | config.proxyGroup[groupName] = &groupItems 194 | } 195 | } 196 | 197 | } 198 | 199 | //[Rule] Section 200 | func loadRules(config *ProxyConfig) { 201 | for _, key := range config.SurgeConfig.Section("Rule").KeyStrings() { 202 | if strings.HasPrefix(key, "//") { 203 | continue 204 | } 205 | items := readArrayLine(key) 206 | ruleName := strings.ToLower(items[0]) 207 | switch ruleName { 208 | case "user-agent": 209 | config.ruleUserAgent = append(config.ruleUserAgent, &Rule{Match: items[1], Action: strings.ToLower(items[2])}) 210 | case "domain-suffix": 211 | config.ruleSuffixDomains = append(config.ruleSuffixDomains, &Rule{Match: items[1], Action: strings.ToLower(items[2])}) 212 | case "domain-prefix": 213 | config.rulePrefixDomains = append(config.rulePrefixDomains, &Rule{Match: items[1], Action: strings.ToLower(items[2])}) 214 | case "domain-keyword": 215 | config.ruleKeywordDomains = append(config.ruleKeywordDomains, &Rule{Match: items[1], Action: strings.ToLower(items[2])}) 216 | case "geoip": 217 | config.ruleGeoIP = append(config.ruleGeoIP, &Rule{Match: items[1], Action: strings.ToLower(items[2])}) 218 | case "final": 219 | config.ruleFinal = &Rule{Match: "final", Action: strings.ToUpper(items[1])} 220 | } 221 | } 222 | } 223 | 224 | func readArrayLine(source string) []string { 225 | out := strings.Split(source, ",") 226 | for i, str := range out { 227 | out[i] = strings.TrimSpace(str) 228 | } 229 | return out 230 | } 231 | -------------------------------------------------------------------------------- /flora/flora.go: -------------------------------------------------------------------------------- 1 | package flora 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "log" 8 | "net" 9 | "regexp" 10 | "strings" 11 | 12 | ss "shadowsocks-go/shadowsocks" 13 | ) 14 | 15 | const ( 16 | ServerTypeShadowSocks = "shadowsocks" 17 | ServerTypeCustom = "custom" 18 | ServerTypeHttp = "http" 19 | ServerTypeHttps = "https" 20 | ServerTypeDirect = "direct" 21 | ServerTypeReject = "direct" 22 | 23 | LocalServerSocksV5 = "localSocksv5" 24 | LocalServerHttp = "localHttp" 25 | 26 | socksVer5 = 5 27 | socksVer4 = 4 28 | httpProxy = 71 29 | socksCmdConnect = 1 30 | 31 | typeIPv4 = 1 // type is ipv4 address 32 | typeDm = 3 // type is domain address 33 | typeIPv6 = 4 // type is ipv6 address 34 | ) 35 | 36 | type ProxyServer interface { 37 | //proxy type 38 | ProxyType() string 39 | //dial 40 | DialWithRawAddr(raw []byte, host string) (remote net.Conn, err error) 41 | // 42 | FailCount() int 43 | 44 | AddFail() 45 | // 46 | ResetFailCount() 47 | } 48 | 49 | type Rule struct { 50 | Match string 51 | Action string 52 | } 53 | 54 | var ( 55 | errAddrType = errors.New("socks addr type not supported") 56 | errVer = errors.New("socks version not supported") 57 | errAuthExtraData = errors.New("socks authentication get extra data") 58 | errReqExtraData = errors.New("socks request get extra data") 59 | errCmd = errors.New("socks command not supported") 60 | errReject = errors.New("socks reject this request") 61 | errSupported = errors.New("proxy type not supported") 62 | errConnect = errors.New("connection remote shadowsocks fail") 63 | errProxy = errors.New("error proxy action") 64 | ) 65 | 66 | var proxyConfig *ProxyConfig 67 | 68 | func Run(surgeCfg, geoipCfg string) { 69 | proxyConfig = LoadConfig(surgeCfg, geoipCfg) 70 | listenAddr := fmt.Sprintf("%s:%d", proxyConfig.LocalHost, proxyConfig.LocalSocksPort) 71 | initProxySettings(proxyConfig.systemBypass, listenAddr) 72 | ln, err := net.Listen("tcp", listenAddr) 73 | if err != nil { 74 | log.Fatal(err) 75 | } 76 | log.Println("Listen socket", listenAddr) 77 | for { 78 | conn, err := ln.Accept() 79 | if err != nil { 80 | log.Println("accept:", err) 81 | continue 82 | } 83 | go handleConnection(conn) 84 | } 85 | } 86 | 87 | func handleConnection(conn net.Conn) { 88 | isClose := false 89 | defer func() { 90 | if !isClose { 91 | conn.Close() 92 | } 93 | }() 94 | var ( 95 | host string 96 | hostType int 97 | err error 98 | rawData []byte 99 | ) 100 | 101 | buf := make([]byte, 1) 102 | io.ReadFull(conn, buf) 103 | 104 | first := buf[0] 105 | switch first { 106 | case socksVer5: 107 | err = handshake(conn, first) 108 | host, hostType, err = socks5Connect(conn) 109 | case socksVer4: 110 | host, hostType, err = socks4Connect(conn, first) 111 | default: 112 | host, hostType, rawData, err = httpProxyConnect(conn, first) 113 | } 114 | if nil != err { 115 | return 116 | } 117 | remote, err := matchRuleAndCreateConn(conn, host, hostType, rawData) 118 | if nil != err { 119 | log.Printf("%v", err) 120 | return 121 | } 122 | //create remote connect 123 | defer func() { 124 | if !isClose { 125 | remote.Close() 126 | } 127 | }() 128 | go ss.PipeThenClose(conn, remote, nil) 129 | ss.PipeThenClose(remote, conn, nil) 130 | isClose = true 131 | } 132 | 133 | func matchRuleAndCreateConn(conn net.Conn, addr string, hostType int, raw []byte) (net.Conn, error) { 134 | if nil == conn { 135 | return nil, errors.New("local connect is nil") 136 | } 137 | host, _, _ := net.SplitHostPort(addr) 138 | var rule *Rule 139 | rule = matchBypass(host) 140 | if nil == rule { 141 | switch hostType { 142 | case typeIPv4, typeIPv6: 143 | rule = matchIpRule(host) 144 | case typeDm: 145 | rule = matchDomainRule(host) 146 | } 147 | } 148 | if nil == rule { 149 | if nil != proxyConfig.ruleFinal { 150 | rule = proxyConfig.ruleFinal 151 | } else { 152 | rule = &Rule{Match: "default", Action: ServerTypeDirect} 153 | } 154 | } 155 | return createRemoteConn(raw, rule, addr) 156 | } 157 | 158 | func matchDomainRule(domain string) *Rule { 159 | for _, rule := range proxyConfig.ruleSuffixDomains { 160 | if strings.HasSuffix(domain, rule.Match) { 161 | return rule 162 | } 163 | } 164 | for _, rule := range proxyConfig.rulePrefixDomains { 165 | if strings.HasPrefix(domain, rule.Match) { 166 | return rule 167 | } 168 | } 169 | for _, rule := range proxyConfig.ruleKeywordDomains { 170 | if strings.Contains(domain, rule.Match) { 171 | return rule 172 | } 173 | } 174 | return nil 175 | } 176 | 177 | func matchIpRule(addr string) *Rule { 178 | ips := resolveRequestIPAddr(addr) 179 | if nil != ips { 180 | country := strings.ToLower(GeoIPs(ips)) 181 | for _, rule := range proxyConfig.ruleGeoIP { 182 | if len(country) != 0 && strings.ToLower(rule.Match) == country { 183 | return rule 184 | } 185 | } 186 | } 187 | return nil 188 | } 189 | 190 | func matchBypass(addr string) *Rule { 191 | ip := net.ParseIP(addr) 192 | for _, h := range proxyConfig.bypassDomains { 193 | var bypass bool = false 194 | var isIp = nil != ip 195 | switch h.(type) { 196 | case net.IP: 197 | if isIp { 198 | bypass = ip.Equal(h.(net.IP)) 199 | } 200 | case *net.IPNet: 201 | if isIp { 202 | bypass = h.(*net.IPNet).Contains(ip) 203 | } 204 | case string: 205 | dm := h.(string) 206 | r, err := regexp.Compile(dm) 207 | if err != nil { 208 | continue 209 | } 210 | bypass = r.MatchString(addr) 211 | } 212 | if bypass { 213 | return &Rule{Match: "bypass", Action: ServerTypeDirect} 214 | } 215 | } 216 | return nil 217 | } 218 | 219 | func createRemoteConn(raw []byte, rule *Rule, host string) (net.Conn, error) { 220 | if server, err := proxyConfig.GetProxyServer(rule.Action); nil == err { 221 | conn, err := server.DialWithRawAddr(raw, host) 222 | if nil != err { 223 | log.Printf("[%s]->[%s] 💊 [%s]", rule.Match, rule.Action, host) 224 | server.AddFail() 225 | } else { 226 | log.Printf("[%s]->[%s] ✅ [%s]", rule.Match, rule.Action, host) 227 | server.ResetFailCount() 228 | } 229 | return conn, err 230 | } 231 | return nil, errConnect 232 | } 233 | -------------------------------------------------------------------------------- /flora/geoip.go: -------------------------------------------------------------------------------- 1 | package flora 2 | 3 | import ( 4 | "log" 5 | "net" 6 | "strings" 7 | 8 | "geoip2-golang" 9 | ) 10 | 11 | var geoDB *geoip2.Reader 12 | 13 | func loadGeoIP(geoFile string) { 14 | db, err := geoip2.Open(geoFile) 15 | // defer db.Close() 16 | if err != nil { 17 | log.Printf("Could not open GeoIP database\n") 18 | } 19 | // log.Println("GeoIP inited.") 20 | geoDB = db 21 | } 22 | 23 | func GeoIPString(ipaddr string) string { 24 | ip := net.ParseIP(ipaddr) 25 | return GeoIP(ip) 26 | } 27 | 28 | func GeoIPs(ips []net.IP) string { 29 | if len(ips) == 0 { 30 | return "" 31 | } 32 | 33 | return GeoIP(ips[0]) 34 | } 35 | 36 | func GeoIP(ip net.IP) string { 37 | // log.Println("Lookup GEO IP", ip) 38 | country, err := geoDB.Country(ip) 39 | if err != nil { 40 | return "" 41 | } 42 | return strings.ToLower(country.Country.IsoCode) 43 | } 44 | 45 | func resolveRequestIPAddr(host string) []net.IP { 46 | var ( 47 | ips []net.IP 48 | err error 49 | ) 50 | ip := net.ParseIP(host) 51 | if nil == ip { 52 | ips, err = net.LookupIP(host) 53 | if err != nil || len(ips) == 0 { 54 | return nil 55 | } 56 | } else { 57 | ips = []net.IP{ip} 58 | } 59 | return ips 60 | } 61 | -------------------------------------------------------------------------------- /flora/http.go: -------------------------------------------------------------------------------- 1 | package flora 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "io" 7 | "net" 8 | "net/http" 9 | "net/http/httputil" 10 | ) 11 | 12 | // local socks server connect 13 | func httpProxyConnect(conn net.Conn, first byte) (addr string, hostType int, raw []byte, err error) { 14 | var ( 15 | HTTP_200 = []byte("HTTP/1.1 200 Connection Established\r\n\r\n") 16 | host string 17 | port string 18 | ) 19 | 20 | buf := make([]byte, 4096) 21 | buf[0] = first 22 | io.ReadAtLeast(conn, buf[1:], 1) 23 | req, err := http.ReadRequest(bufio.NewReader(bytes.NewReader(buf))) 24 | if nil != err { 25 | return 26 | } 27 | host, port, err = net.SplitHostPort(req.Host) 28 | if nil != err { 29 | host = req.Host 30 | port = req.URL.Port() 31 | } 32 | scheme := req.URL.Scheme 33 | if "" == port { 34 | if scheme == "http" { 35 | port = "80" 36 | } else { 37 | port = "443" 38 | } 39 | } 40 | addr = net.JoinHostPort(host, port) 41 | method := req.Method 42 | hostType = getRequestType(addr) 43 | switch method { 44 | case http.MethodConnect: 45 | _, err = conn.Write(HTTP_200) 46 | default: 47 | removeHeaders(req) 48 | raw, err = httputil.DumpRequest(req, true) 49 | } 50 | return 51 | } 52 | 53 | func getRequestType(addr string) int { 54 | host, _, _ := net.SplitHostPort(addr) 55 | ip := net.ParseIP(host) 56 | if nil != ip { 57 | return typeIPv4 58 | } 59 | return typeDm 60 | } 61 | 62 | func removeHeaders(req *http.Request) { 63 | req.RequestURI = "" 64 | req.Header.Del("Accept-Encoding") 65 | // curl can add that, see 66 | // https://jdebp.eu./FGA/web-proxy-connection-header.html 67 | req.Header.Del("Proxy-Connection") 68 | req.Header.Del("Proxy-Authenticate") 69 | req.Header.Del("Proxy-Authorization") 70 | //req.Header.Del("Referer") 71 | // Connection, Authenticate and Authorization are single hop Header: 72 | // http://www.w3.org/Protocols/rfc2616/rfc2616.txt 73 | // 14.10 Connection 74 | // The Connection general-header field allows the sender to specify 75 | // options that are desired for that particular connection and MUST NOT 76 | // be communicated by proxies over further connections. 77 | req.Header.Del("Connection") 78 | } 79 | -------------------------------------------------------------------------------- /flora/network_setup.go: -------------------------------------------------------------------------------- 1 | package flora 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "os/signal" 7 | "runtime" 8 | "syscall" 9 | "time" 10 | ) 11 | 12 | type SystemProxySettings interface { 13 | TurnOnGlobProxy() 14 | TurnOffGlobProxy() 15 | } 16 | 17 | var sigs = make(chan os.Signal, 1) 18 | 19 | func resetProxySettings(proxySettings SystemProxySettings) { 20 | for { 21 | select { 22 | case <-sigs: 23 | log.Print("Flora-kit is shutdown now ...") 24 | if nil != proxySettings { 25 | proxySettings.TurnOffGlobProxy() 26 | } 27 | time.Sleep(time.Duration(2000)) 28 | os.Exit(0) 29 | } 30 | } 31 | } 32 | 33 | func initProxySettings(bypass []string, addr string) { 34 | signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) 35 | var proxySettings SystemProxySettings 36 | if runtime.GOOS == "windows" { 37 | w := &windows{addr} 38 | proxySettings = w 39 | } else if runtime.GOOS == "darwin" { 40 | d := &darwin{bypass, addr} 41 | proxySettings = d 42 | } 43 | if nil != proxySettings { 44 | proxySettings.TurnOnGlobProxy() 45 | } 46 | go resetProxySettings(proxySettings) 47 | } 48 | -------------------------------------------------------------------------------- /flora/network_setup_mac.go: -------------------------------------------------------------------------------- 1 | package flora 2 | 3 | import ( 4 | "bytes" 5 | "log" 6 | "net" 7 | "os/exec" 8 | "strings" 9 | ) 10 | 11 | type darwin struct { 12 | bypassDomains []string 13 | address string 14 | } 15 | 16 | type execNetworkFunc func(name string) 17 | 18 | var allow_services = "Wi-Fi|Thunderbolt Bridge|Thunderbolt Ethernet" 19 | 20 | func (d *darwin) TurnOffGlobProxy() { 21 | execNetworks(func(name string) { 22 | runNetworksetup("-setftpproxystate", name, "off") 23 | runNetworksetup("-setwebproxystate", name, "off") 24 | runNetworksetup("-setsecurewebproxystate", name, "off") 25 | runNetworksetup("-setstreamingproxystate", name, "off") 26 | runNetworksetup("-setgopherproxystate", name, "off") 27 | runNetworksetup("-setsocksfirewallproxystate", name, "off") 28 | runNetworksetup("-setproxyautodiscovery", name, "off") 29 | }) 30 | } 31 | 32 | func (d *darwin) TurnOnGlobProxy() { 33 | host, port, _ := net.SplitHostPort(d.address) 34 | 35 | execNetworks(func(name string) { 36 | runNetworksetup("-setsocksfirewallproxy", name, host, port) 37 | }) 38 | 39 | execNetworks(func(name string) { 40 | args := []string{"-setproxybypassdomains", name} 41 | args = append(args, d.bypassDomains...) 42 | runNetworksetup(args...) 43 | }) 44 | } 45 | 46 | func runNetworksetup(args ...string) string { 47 | 48 | // log.Println("networksetup", args) 49 | cmd := exec.Command("networksetup", args...) 50 | var out, stderr bytes.Buffer 51 | cmd.Stdout = &out 52 | cmd.Stderr = &stderr 53 | err := cmd.Run() 54 | if err != nil { 55 | log.Println(err) 56 | log.Println(stderr.String()) 57 | } 58 | return out.String() 59 | } 60 | 61 | func execNetworks(callback execNetworkFunc) { 62 | for _, name := range listNetworks() { 63 | if !strings.Contains(allow_services, name) { 64 | continue 65 | } 66 | callback(name) 67 | } 68 | } 69 | 70 | func listNetworks() (networks []string) { 71 | out := runNetworksetup("-listallnetworkservices") 72 | out = strings.TrimSpace(out) 73 | networks = strings.Split(out, "\n") 74 | return 75 | } 76 | -------------------------------------------------------------------------------- /flora/network_setup_win.go: -------------------------------------------------------------------------------- 1 | package flora 2 | 3 | import ( 4 | "log" 5 | "os/exec" 6 | ) 7 | 8 | type windows struct { 9 | address string 10 | } 11 | 12 | const ( 13 | cmdRegistry = `reg` 14 | cmdRegistryAdd = `add` 15 | internetSettingsKey = `HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings` 16 | keyProxyEnable = `ProxyEnable` 17 | keyProxyServer = `ProxyServer` 18 | dataTypeDWord = `REG_DWORD` 19 | dataTypeRegSZ = `REG_SZ` 20 | ) 21 | 22 | func (w *windows) TurnOnGlobProxy() { 23 | c := exec.Command(cmdRegistry, cmdRegistryAdd, internetSettingsKey, `/v`, keyProxyEnable, `/t`, dataTypeDWord, `/d`, `1`, `/f`) 24 | var err error 25 | if _, err = c.CombinedOutput(); err != nil { 26 | log.Printf("enable windows proxy has error %s", err) 27 | } 28 | c = exec.Command(cmdRegistry, cmdRegistryAdd, internetSettingsKey, `/v`, keyProxyServer, `/t`, dataTypeRegSZ, `/d`, w.address, `/f`) 29 | if _, err = c.CombinedOutput(); err != nil { 30 | log.Printf("Windows global proxy settings has error %s , Try to set it manually ", err) 31 | } 32 | if nil == err { 33 | log.Print("Windows global proxy settings are successful ,Please use after 2 minutes ...") 34 | } 35 | } 36 | 37 | // TurnOffSystemProxy 38 | func (w *windows) TurnOffGlobProxy() { 39 | var err error 40 | c := exec.Command(cmdRegistry, cmdRegistryAdd, internetSettingsKey, `/v`, keyProxyEnable, `/t`, dataTypeDWord, `/d`, `0`, `/f`) 41 | if _, err = c.CombinedOutput(); err != nil { 42 | log.Printf("disable windows proxy has error %s", err) 43 | } 44 | if nil == err { 45 | log.Print("disable windows proxy settings are successful ...") 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /flora/proxy_direct.go: -------------------------------------------------------------------------------- 1 | package flora 2 | 3 | import "net" 4 | 5 | type DirectServer struct { 6 | proxyType string 7 | } 8 | 9 | func NewDirect() *DirectServer { 10 | return &DirectServer{proxyType: ServerTypeDirect} 11 | } 12 | 13 | func (s *DirectServer) FailCount() int { 14 | return 0 15 | } 16 | 17 | func (s *DirectServer) ResetFailCount() { 18 | 19 | } 20 | 21 | func (s *DirectServer) AddFail() { 22 | 23 | } 24 | 25 | func (s *DirectServer) ProxyType() string { 26 | return s.proxyType 27 | } 28 | 29 | func (s *DirectServer) DialWithRawAddr(raw []byte, host string) (remote net.Conn, err error) { 30 | conn, err := net.Dial("tcp", host) 31 | if nil != err { 32 | return nil, err 33 | } 34 | if nil != raw && len(raw) > 0 { 35 | conn.Write(raw) 36 | } 37 | return conn, err 38 | } 39 | -------------------------------------------------------------------------------- /flora/proxy_reject.go: -------------------------------------------------------------------------------- 1 | package flora 2 | 3 | import "net" 4 | 5 | type Reject struct { 6 | proxyType string 7 | } 8 | 9 | func NewReject() *Reject { 10 | return &Reject{proxyType: ServerTypeReject} 11 | } 12 | 13 | func (s *Reject) FailCount() int { 14 | return 0 15 | } 16 | 17 | func (s *Reject) ResetFailCount() { 18 | 19 | } 20 | 21 | func (s *Reject) AddFail() { 22 | 23 | } 24 | 25 | func (s *Reject) ProxyType() string { 26 | return s.proxyType 27 | } 28 | 29 | func (s *Reject) DialWithRawAddr(raw []byte, host string) (remote net.Conn, err error) { 30 | return nil, errReject 31 | } 32 | -------------------------------------------------------------------------------- /flora/proxy_shadowsocks.go: -------------------------------------------------------------------------------- 1 | package flora 2 | 3 | import ( 4 | ss "shadowsocks-go/shadowsocks" 5 | "net" 6 | "sync" 7 | ) 8 | 9 | type ShadowSocksServer struct { 10 | proxyType string 11 | server string 12 | cipher *ss.Cipher 13 | failCount int 14 | lock sync.RWMutex 15 | } 16 | 17 | func NewShadowSocks(server string, cipher *ss.Cipher) *ShadowSocksServer { 18 | return &ShadowSocksServer{ 19 | proxyType: ServerTypeShadowSocks, 20 | server: server, 21 | cipher: cipher, 22 | } 23 | } 24 | 25 | func (s *ShadowSocksServer) ResetFailCount() { 26 | s.lock.Lock() 27 | defer s.lock.Unlock() 28 | s.failCount = 0 29 | } 30 | 31 | func (s *ShadowSocksServer) AddFail() { 32 | s.failCount++ 33 | } 34 | 35 | func (s *ShadowSocksServer) FailCount() int { 36 | s.lock.RLock() 37 | defer s.lock.RUnlock() 38 | return s.failCount 39 | } 40 | 41 | func (s *ShadowSocksServer) ProxyType() string { 42 | return s.proxyType 43 | } 44 | 45 | func (s *ShadowSocksServer) DialWithRawAddr(raw []byte, host string) (net.Conn, error) { 46 | if nil != raw && len(raw) > 0 { 47 | return ss.DialWithRawAddr(raw, s.server, s.cipher.Copy()) 48 | } else { 49 | return ss.Dial(host, s.server, s.cipher.Copy()) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /flora/socks4.go: -------------------------------------------------------------------------------- 1 | package flora 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "io" 7 | "net" 8 | ) 9 | 10 | /* 11 | socks4 protocol 12 | 13 | request 14 | byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | ... | 15 | |0x04|cmd| port | ip | user\0 | 16 | 17 | reply 18 | byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7| 19 | |0x00|status| | | 20 | 21 | 22 | socks4a protocol 23 | 24 | request 25 | byte | 0 | 1 | 2 | 3 |4 | 5 | 6 | 7 | 8 | ... |... | 26 | |0x04|cmd| port | 0.0.0.x | user\0 |domain\0| 27 | 28 | reply 29 | byte | 0 | 1 | 2 | 3 | 4 | 5 | 6| 7 | 30 | |0x00|staus| port | ip | 31 | 32 | */ 33 | 34 | // local socks server connect 35 | func socks4Connect(conn net.Conn, first byte) (addr string, hostType int, err error) { 36 | const ( 37 | idVer = 0 38 | idStatus = 1 39 | idPort = 2 // address type index 40 | idPortLen = 2 41 | idIP = 4 // ip addres start index 42 | idIPLen = 4 // domain address length index 43 | 44 | idVariable = 8 45 | id4aFixLen = 8 46 | cmdConnect = 1 47 | ) 48 | // refer to getRequest in flora.go for why set buffer size to 263 49 | buf := make([]byte, 128) 50 | buf[idVer] = first 51 | var n int 52 | // read till we get possible domain length field 53 | if n, err = io.ReadAtLeast(conn, buf[1:], id4aFixLen); err != nil { 54 | return 55 | } 56 | n++ 57 | // command only support connect 58 | if buf[idStatus] != cmdConnect { 59 | return 60 | } 61 | // get port 62 | port := binary.BigEndian.Uint16(buf[idPort : idPort+idPortLen]) 63 | 64 | // get ip 65 | ip := net.IP(buf[idIP : idIP+idIPLen]) 66 | hostType = typeIPv4 67 | var host = ip.String() 68 | 69 | //socks4a 70 | if ip[0] == 0x00 && ip[1] == 0x00 && ip[2] == 0x00 && ip[3] != 0x00 && n+1 >= id4aFixLen { 71 | dm := buf[idVariable:n] 72 | host = string(dm) 73 | hostType = typeDm 74 | } 75 | addr = net.JoinHostPort(host, fmt.Sprintf("%d", port)) 76 | _, err = conn.Write([]byte{0x00, 0x5a, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00}) 77 | return 78 | } 79 | -------------------------------------------------------------------------------- /flora/socks5.go: -------------------------------------------------------------------------------- 1 | package flora 2 | 3 | import ( 4 | "encoding/binary" 5 | ss "shadowsocks-go/shadowsocks" 6 | "io" 7 | "log" 8 | "net" 9 | "strconv" 10 | ) 11 | 12 | /* 13 | socks5 protocol 14 | 15 | initial 16 | 17 | byte | 0 | 1 | 2 | ...... | n | 18 | |0x05|num auth| auth methods | 19 | 20 | 21 | reply 22 | 23 | byte | 0 | 1 | 24 | |0x05| auth| 25 | 26 | 27 | username/password auth request 28 | 29 | byte | 0 | 1 | | 1 byte | | 30 | |0x01|username_len| username | password_len | password | 31 | 32 | username/password auth reponse 33 | 34 | byte | 0 | 1 | 35 | |0x01|status| 36 | 37 | request 38 | 39 | byte | 0 | 1 | 2 | 3 | 4 | .. | n-2 | n-1| n | 40 | |0x05|cmd|0x00|addrtype| addr | port | 41 | 42 | response 43 | byte |0 | 1 | 2 | 3 | 4 | .. | n-2 | n-1 | n | 44 | |0x05|status|0x00|addrtype| addr | port | 45 | 46 | */ 47 | 48 | //local socks server auth 49 | func handshake(conn net.Conn, first byte) (err error) { 50 | const ( 51 | idVer = 0 52 | idNmethod = 1 53 | ) 54 | // version identification and method selection message in theory can have 55 | // at most 256 methods, plus version and nmethod field in total 258 bytes 56 | // the current rfc defines only 3 authentication methods (plus 2 reserved), 57 | // so it won't be such long in practice 58 | 59 | buf := make([]byte, 258) 60 | buf[idVer] = first 61 | var n int 62 | ss.SetReadTimeout(conn) 63 | // make sure we get the nmethod field 64 | if n, err = io.ReadAtLeast(conn, buf[1:], idNmethod+1); err != nil { 65 | return 66 | } 67 | n++ 68 | //if buf[idVer] != socksVer5 { 69 | // return errVer 70 | //} 71 | nmethod := int(buf[idNmethod]) 72 | msgLen := nmethod + 2 73 | if n == msgLen { // handshake done, common case 74 | // do nothing, jump directly to send confirmation 75 | } else if n < msgLen { // has more methods to read, rare case 76 | if _, err = io.ReadFull(conn, buf[n:msgLen]); err != nil { 77 | log.Print(err) 78 | return 79 | } 80 | } else { // error, should not get extra data 81 | return errAuthExtraData 82 | } 83 | // send confirmation: version 5, no authentication required 84 | _, err = conn.Write([]byte{socksVer5, 0}) 85 | return 86 | } 87 | 88 | // local socks server connect 89 | func socks5Connect(conn net.Conn) (host string, hostType int, err error) { 90 | const ( 91 | idVer = 0 92 | idCmd = 1 93 | idType = 3 // address type index 94 | idIP0 = 4 // ip addres start index 95 | idDmLen = 4 // domain address length index 96 | idDm0 = 5 // domain address start index 97 | 98 | lenIPv4 = 3 + 1 + net.IPv4len + 2 // 3(ver+cmd+rsv) + 1addrType + ipv4 + 2port 99 | lenIPv6 = 3 + 1 + net.IPv6len + 2 // 3(ver+cmd+rsv) + 1addrType + ipv6 + 2port 100 | lenDmBase = 3 + 1 + 1 + 2 // 3 + 1addrType + 1addrLen + 2port, plus addrLen 101 | ) 102 | // refer to getRequest in flora.go for why set buffer size to 263 103 | buf := make([]byte, 263) 104 | var n int 105 | ss.SetReadTimeout(conn) 106 | // read till we get possible domain length field 107 | if n, err = io.ReadAtLeast(conn, buf, idDmLen+1); err != nil { 108 | return 109 | } 110 | // check version and cmd 111 | //if buf[idVer] != socksVer5 { 112 | // err = errVer 113 | // return 114 | //} 115 | if buf[idCmd] != socksCmdConnect { 116 | err = errCmd 117 | return 118 | } 119 | 120 | reqLen := -1 121 | hostType = int(buf[idType]) 122 | switch hostType { 123 | case typeIPv4: 124 | reqLen = lenIPv4 125 | case typeIPv6: 126 | reqLen = lenIPv6 127 | case typeDm: 128 | reqLen = int(buf[idDmLen]) + lenDmBase 129 | default: 130 | err = errAddrType 131 | return 132 | } 133 | 134 | if n == reqLen { 135 | // common case, do nothing 136 | } else if n < reqLen { // rare case 137 | if _, err = io.ReadFull(conn, buf[n:reqLen]); err != nil { 138 | return 139 | } 140 | } else { 141 | err = errReqExtraData 142 | return 143 | } 144 | 145 | //raw := buf[idType:reqLen] 146 | switch hostType { 147 | case typeIPv4: 148 | host = net.IP(buf[idIP0 : idIP0+net.IPv4len]).String() 149 | case typeIPv6: 150 | host = net.IP(buf[idIP0 : idIP0+net.IPv6len]).String() 151 | case typeDm: 152 | host = string(buf[idDm0 : idDm0+buf[idDmLen]]) 153 | } 154 | port := binary.BigEndian.Uint16(buf[reqLen-2 : reqLen]) 155 | host = net.JoinHostPort(host, strconv.Itoa(int(port))) 156 | _, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x08, 0x43}) 157 | return 158 | } 159 | -------------------------------------------------------------------------------- /geoip.mmdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huacnlee/flora-kit/521a8c25267531c285837217f452d6a374322735/geoip.mmdb -------------------------------------------------------------------------------- /geoip_test.go: -------------------------------------------------------------------------------- 1 | package main_test 2 | 3 | import ( 4 | "github.com/huacnlee/flora-kit/flora" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | func TestGeoIP(t *testing.T) { 10 | p, _ := os.Getwd() 11 | flora.LoadConfig("", p+"/geoip.mmdb") 12 | 13 | if flora.GeoIPString("121.0.29.91") != "cn" { 14 | t.Errorf("121.0.29.91 should be cn") 15 | } 16 | 17 | if flora.GeoIPString("218.253.0.89") != "hk" { 18 | t.Errorf("218.253.0.89 should be hk") 19 | } 20 | 21 | if flora.GeoIPString("218.176.242.11") != "jp" { 22 | t.Errorf("218.176.242.11 should be jp") 23 | } 24 | 25 | if flora.GeoIPString("8.8.8.8") != "us" { 26 | t.Errorf("218.176.242.11 should be jp") 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /goreleaser.yml: -------------------------------------------------------------------------------- 1 | # goreleaser.yml 2 | # Build customization 3 | build: 4 | main: main.go 5 | binary: flora-kit 6 | goos: 7 | - windows 8 | - darwin 9 | - linux 10 | goarch: 11 | - amd64 12 | ldflags: -s -w 13 | release: 14 | github: 15 | owner: huacnlee 16 | name: flora-kit 17 | # Archive customization 18 | archive: 19 | format: tar.gz 20 | replacements: 21 | amd64: 64-bit 22 | darwin: macOS 23 | files: 24 | - README.md 25 | - LICENSE 26 | - CHANGELOG.md 27 | - flora.default.conf 28 | - geoip.mmdb -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | 6 | "github.com/huacnlee/flora-kit/flora" 7 | ) 8 | 9 | func main() { 10 | var configFile, geoipdb string 11 | flag.StringVar(&configFile, "s", "flora.default.conf", "specify surge config file") 12 | flag.StringVar(&geoipdb, "d", "geoip.mmdb", "specify geoip db file") 13 | flag.Parse() 14 | flora.Run(configFile, geoipdb) 15 | 16 | } 17 | --------------------------------------------------------------------------------