├── shrimp.png ├── util ├── go.mod ├── go.sum └── mkpasswd.go ├── go.mod ├── go.sum ├── shrimp.conf ├── LICENSE.md ├── shrimp_test.go ├── README.md └── shrimp.go /shrimp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ipv6rslimited/shrimp/HEAD/shrimp.png -------------------------------------------------------------------------------- /util/go.mod: -------------------------------------------------------------------------------- 1 | module mkpasswd 2 | 3 | go 1.22.3 4 | 5 | require golang.org/x/crypto v0.23.0 6 | -------------------------------------------------------------------------------- /util/go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= 2 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= 3 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module shrimp 2 | 3 | go 1.22.3 4 | 5 | require ( 6 | github.com/ipv6rslimited/lrucache v1.0.0 7 | github.com/ipv6rslimited/peter v1.0.4 8 | golang.org/x/crypto v0.23.0 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/ipv6rslimited/lrucache v1.0.0 h1:Ns9vpipQLPd3E9MHIjkNa4RFoo4/v/Jtr2s5SkWJVnE= 2 | github.com/ipv6rslimited/lrucache v1.0.0/go.mod h1:Xt4GFTJyEZ19s9uPKevgwTtuAW2vMfLyulX/AEDyKe4= 3 | github.com/ipv6rslimited/peter v1.0.4 h1:36MoZUIUFC0MMIeQwq8nK7BbK53tOf8+t0ALnm8hpUE= 4 | github.com/ipv6rslimited/peter v1.0.4/go.mod h1:Oap/pZXH8U9gJJY117QYXSuKjH5i8ENDWXL2W6VbzGo= 5 | golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= 6 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= 7 | -------------------------------------------------------------------------------- /shrimp.conf: -------------------------------------------------------------------------------- 1 | { 2 | "listenAddrs": ["[::]:443"], 3 | "plaintextAddr": "127.0.0.1:3128", 4 | "lockdownMode": true, 5 | "ipv6Interface": "wg0", 6 | "dns64Server": "2606:4700:4700::64", 7 | "credentialsFile": "/etc/shrimp/passwd", 8 | "debugMode": true, 9 | "certFile": "/etc/letsencrypt/live/proxy.example.com/fullchain.pem", 10 | "keyFile": "/etc/letsencrypt/live/proxy.example.com/privkey.pem", 11 | "ipv4Translator": "visibleip.com", 12 | "dnsCacheCapacity": 100, 13 | "dnsTTL": 300, 14 | "allowedHosts": [".*"], 15 | "disallowedHosts": [ 16 | "^localhost$", 17 | "^127\\.0\\.0\\.1$", 18 | "^10\\.", 19 | "^172\\.(1[6-9]|2[0-9]|3[0-1])\\.", 20 | "^192\\.168\\." 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # Commercial Operators Open Source License 2 | ## COOL 3 | 4 | ### Version 1.000.000 5 | 6 | 7 | ### Preamble 8 | 9 | In the spirit of innovation, collaboration, and the enduring belief in the 10 | freedom to use, study, share, and improve software, we introduce the 11 | Commercial Operators Open Source License (COOL). This license is crafted with 12 | the hope of fostering a vibrant ecosystem where commercial entities and 13 | individual contributors alike can contribute to a flourishing open source 14 | community. 15 | 16 | 17 | ### Definitions 18 | 19 | "The License" refers to the terms and conditions for the use, copying, 20 | distribution, and modification set forth in this document. 21 | 22 | "The Software" refers to the covered work governed by this License. 23 | 24 | "You" refers to any individual or legal entity exercising permissions granted 25 | by this License. 26 | 27 | 28 | ### Grant of Rights 29 | 30 | With the goal of fostering an open and collaborative world, the authors of 31 | the Software hereby grant you a worldwide, royalty-free, non-exclusive, 32 | perpetual license, subject to the terms of this License, to: 33 | 34 | a) Use, copy, modify, merge, publish, distribute, sublicense, and/or sell 35 | copies of the Software. 36 | 37 | b) Permit persons to whom the Software is furnished to do likewise, subject 38 | to the following conditions. 39 | 40 | 41 | ### Conditions 42 | 43 | Your exercise of the granted rights is conditioned upon compliance with the 44 | following: 45 | 46 | a) Preservation of copyright notices, this License, and the disclaimer 47 | provided, within all copies of the Software or substantial portions thereof. 48 | 49 | b) Inclusion of a clear attribution to the original authors, preserving all 50 | notices of copyright, patent, or trademark rights, specifically including 51 | author information and a direct website link as specified by the authors. 52 | 53 | c) Ensuring that any modifications, including derivative works, are offered 54 | under the same terms of this License, without additional restrictions. 55 | 56 | d) Acknowledgment that the use of the Software's name or contributors' names 57 | for endorsement of derived products without prior written consent is not 58 | permitted. 59 | 60 | 61 | ### Disclaimer of Warranty 62 | 63 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 64 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 65 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 66 | AUTHORS, COPYRIGHT HOLDERS OR THEIR CONSTITUENTS BE LIABLE FOR ANY CLAIM, 67 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 68 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 69 | OR OTHER DEALINGS IN THE SOFTWARE. 70 | 71 | 72 | ### Termination 73 | 74 | If you violate any term of this License, your rights under this License will 75 | terminate automatically. Upon termination, all rights granted to you under 76 | this License cease, and you must cease all use, distribution, and development 77 | of the Software. 78 | 79 | -------------------------------------------------------------------------------- /util/mkpasswd.go: -------------------------------------------------------------------------------- 1 | /* 2 | ** 3 | ** mkpasswd 4 | ** Make and edit passwords in a passwd file for shrimp 5 | ** 6 | ** Distributed under the COOL License. 7 | ** 8 | ** Copyright (c) 2024 IPv6.rs 9 | ** All Rights Reserved 10 | ** 11 | */ 12 | 13 | package main 14 | 15 | import ( 16 | "bufio" 17 | "flag" 18 | "fmt" 19 | "os" 20 | "strings" 21 | "golang.org/x/crypto/bcrypt" 22 | ) 23 | 24 | func main() { 25 | createFlag := flag.Bool("create", false, "Create a user and password") 26 | editFlag := flag.Bool("edit", false, "Edit an existing user's password") 27 | passwdFlag := flag.String("passwd", "passwd", "Path to passwords file") 28 | 29 | flag.Parse() 30 | 31 | if *createFlag || *editFlag { 32 | if len(flag.Args()) != 2 { 33 | usage() 34 | } 35 | 36 | username := flag.Arg(0) 37 | password := flag.Arg(1) 38 | passwdFile := *passwdFlag 39 | 40 | if *createFlag { 41 | createPassword(username, password, passwdFile) 42 | } else if *editFlag { 43 | editPassword(username, password, passwdFile) 44 | } else { 45 | usage() 46 | } 47 | } else { 48 | usage() 49 | } 50 | } 51 | 52 | func createPassword(username, password, passwdFile string) { 53 | if _, err := os.Stat(passwdFile); err == nil { 54 | if userExists(username, passwdFile) { 55 | fmt.Println("Error: User already exists.") 56 | os.Exit(1) 57 | } 58 | } 59 | 60 | hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) 61 | if err != nil { 62 | fmt.Println("Error hashing password:", err) 63 | os.Exit(1) 64 | } 65 | 66 | file, err := os.OpenFile(passwdFile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) 67 | if err != nil { 68 | fmt.Println("Error opening passwd file:", err) 69 | os.Exit(1) 70 | } 71 | defer file.Close() 72 | 73 | if _, err = file.WriteString(fmt.Sprintf("%s:%s\n", username, hashedPassword)); err != nil { 74 | fmt.Println("Error writing to passwd file:", err) 75 | os.Exit(1) 76 | } 77 | } 78 | 79 | func editPassword(username, password, passwdFile string) { 80 | if !userExists(username, passwdFile) { 81 | fmt.Println("Error: User does not exist.") 82 | os.Exit(1) 83 | } 84 | 85 | hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) 86 | if err != nil { 87 | fmt.Println("Error hashing password:", err) 88 | os.Exit(1) 89 | } 90 | 91 | lines, err := readPasswdFile(passwdFile) 92 | if err != nil { 93 | fmt.Println("Error reading passwd file:", err) 94 | os.Exit(1) 95 | } 96 | 97 | file, err := os.OpenFile(passwdFile, os.O_WRONLY|os.O_TRUNC, 0600) 98 | if err != nil { 99 | fmt.Println("Error opening passwd file:", err) 100 | os.Exit(1) 101 | } 102 | defer file.Close() 103 | 104 | for _, line := range lines { 105 | parts := strings.Split(line, ":") 106 | if parts[0] == username { 107 | line = fmt.Sprintf("%s:%s", username, hashedPassword) 108 | } 109 | if _, err = file.WriteString(line + "\n"); err != nil { 110 | fmt.Println("Error writing to passwd file:", err) 111 | os.Exit(1) 112 | } 113 | } 114 | } 115 | 116 | func userExists(username, passwdFile string) bool { 117 | file, err := os.Open(passwdFile) 118 | if err != nil { 119 | fmt.Println("Error opening passwd file:", err) 120 | os.Exit(1) 121 | } 122 | defer file.Close() 123 | 124 | scanner := bufio.NewScanner(file) 125 | for scanner.Scan() { 126 | line := scanner.Text() 127 | parts := strings.Split(line, ":") 128 | if len(parts) != 2 { 129 | continue 130 | } 131 | if parts[0] == username { 132 | return true 133 | } 134 | } 135 | 136 | if err := scanner.Err(); err != nil { 137 | fmt.Println("Error reading passwd file:", err) 138 | os.Exit(1) 139 | } 140 | 141 | return false 142 | } 143 | 144 | func readPasswdFile(passwdFile string) ([]string, error) { 145 | file, err := os.Open(passwdFile) 146 | if err != nil { 147 | return nil, err 148 | } 149 | defer file.Close() 150 | 151 | var lines []string 152 | scanner := bufio.NewScanner(file) 153 | for scanner.Scan() { 154 | lines = append(lines, scanner.Text()) 155 | } 156 | 157 | if err := scanner.Err(); err != nil { 158 | return nil, err 159 | } 160 | 161 | return lines, nil 162 | } 163 | 164 | func usage() { 165 | fmt.Println("Usage: mkpasswd -passwd -create ") 166 | fmt.Println(" mkpasswd -passwd -edit ") 167 | os.Exit(1) 168 | } 169 | -------------------------------------------------------------------------------- /shrimp_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | ** 3 | ** shrimp_test 4 | ** Tests for shrimp 5 | ** 6 | ** Distributed under the COOL License. 7 | ** 8 | ** Copyright (c) 2024 IPv6.rs 9 | ** All Rights Reserved 10 | ** 11 | */ 12 | 13 | package main 14 | 15 | import ( 16 | "encoding/base64" 17 | "testing" 18 | "net" 19 | "golang.org/x/crypto/bcrypt" 20 | "github.com/ipv6rslimited/lrucache" 21 | "os" 22 | ) 23 | 24 | func TestStripPort(t *testing.T) { 25 | tests := []struct { 26 | input string 27 | output string 28 | }{ 29 | {"example.com:80", "example.com"}, 30 | {"example.com", "example.com"}, 31 | {"example.com:", "example.com"}, 32 | {"127.0.0.1:8080", "127.0.0.1"}, 33 | {"[::1]:443", "[::1]"}, 34 | } 35 | 36 | for _, test := range tests { 37 | result := stripPort(test.input) 38 | if result != test.output { 39 | t.Errorf("stripPort(%s) = %s; want %s", test.input, result, test.output) 40 | } 41 | } 42 | } 43 | 44 | func TestGetHostPort(t *testing.T) { 45 | tests := []struct { 46 | input string 47 | defaultPort string 48 | host string 49 | port string 50 | }{ 51 | {"example.com:80", "443", "example.com", "80"}, 52 | {"example.com", "443", "example.com", "443"}, 53 | {"example.com:", "443", "example.com", "443"}, 54 | {"127.0.0.1:8080", "443", "127.0.0.1", "8080"}, 55 | {"[::1]:443", "443", "[::1]", "443"}, 56 | } 57 | 58 | for _, test := range tests { 59 | host, port := getHostPort(test.input, test.defaultPort) 60 | if host != test.host || port != test.port { 61 | t.Errorf("getHostPort(%s, %s) = (%s, %s); want (%s, %s)", test.input, test.defaultPort, host, port, test.host, test.port) 62 | } 63 | } 64 | } 65 | 66 | func TestIsIPv4Address(t *testing.T) { 67 | tests := []struct { 68 | input string 69 | output bool 70 | }{ 71 | {"127.0.0.1", true}, 72 | {"::1", false}, 73 | {"192.168.0.1", true}, 74 | {"example.com", false}, 75 | {"2001:db8::ff00:42:8329", false}, 76 | } 77 | 78 | for _, test := range tests { 79 | result := isIPv4Address(test.input) 80 | if result != test.output { 81 | t.Errorf("isIPv4Address(%s) = %t; want %t", test.input, result, test.output) 82 | } 83 | } 84 | } 85 | 86 | func TestRemoveAuthHeader(t *testing.T) { 87 | input := "GET / HTTP/1.1\r\nHost: example.com\r\nProxy-Authorization: Basic dXNlcjpwYXNz\r\nConnection: close\r\n\r\n" 88 | expected := "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n" 89 | result := removeAuthHeader([]byte(input)) 90 | if string(result) != expected { 91 | t.Errorf("removeAuthHeader() = %s; want %s", result, expected) 92 | } 93 | } 94 | 95 | func TestCheckAuth(t *testing.T) { 96 | username := "user" 97 | password := "pass" 98 | hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) 99 | users = map[string]string{ 100 | username: string(hashedPassword), 101 | } 102 | 103 | validAuth := "Basic " + base64.StdEncoding.EncodeToString([]byte(username+":"+password)) 104 | authenticated, promptPassword := checkAuth(validAuth) 105 | if !authenticated || promptPassword { 106 | t.Errorf("checkAuth() with valid credentials failed") 107 | } 108 | 109 | invalidAuth := "Basic " + base64.StdEncoding.EncodeToString([]byte(username+":wrongpass")) 110 | authenticated, promptPassword = checkAuth(invalidAuth) 111 | if authenticated || !promptPassword { 112 | t.Errorf("checkAuth() with invalid credentials passed") 113 | } 114 | 115 | authenticated, promptPassword = checkAuth("") 116 | if authenticated || !promptPassword { 117 | t.Errorf("checkAuth() with no credentials passed") 118 | } 119 | 120 | nonBasicAuth := "Bearer " + base64.StdEncoding.EncodeToString([]byte(username+":"+password)) 121 | authenticated, promptPassword = checkAuth(nonBasicAuth) 122 | if authenticated || !promptPassword { 123 | t.Errorf("checkAuth() with non-basic auth passed") 124 | } 125 | 126 | users = nil 127 | } 128 | 129 | func TestConnectTo(t *testing.T) { 130 | host, port := "www.google.com", "80" 131 | conn, err := connectTo(host, port) 132 | if err != nil { 133 | t.Errorf("connectTo(%s, %s) failed: %v", host, port, err) 134 | } 135 | if conn != nil { 136 | conn.Close() 137 | } 138 | } 139 | 140 | func TestLookupWithCache(t *testing.T) { 141 | hostname := "localhost" 142 | ip, err := lookupWithCache(hostname) 143 | if err != nil { 144 | t.Errorf("lookupWithCache(%s) failed: %v", hostname, err) 145 | } 146 | if net.ParseIP(ip) == nil { 147 | t.Errorf("lookupWithCache(%s) returned invalid IP: %s", hostname, ip) 148 | } 149 | } 150 | 151 | func TestMain(m *testing.M) { 152 | config = Config{ 153 | DNSCacheCapacity: 10, 154 | DNSTTL: 60, 155 | DebugMode: false, 156 | } 157 | setLogger(config.DebugMode) 158 | 159 | dnsCache = lrucache.NewLRUCache(config.DNSCacheCapacity) 160 | 161 | code := m.Run() 162 | 163 | os.Exit(code) 164 | } 165 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![shrimp logo](https://raw.githubusercontent.com/ipv6rslimited/shrimp/main/shrimp.png) 2 | 3 | # shrimp 4 | 5 | **shrimp** is a simple forward proxy written in GoLang, that does not decrypt traffic, making it secure and easy to configure. It features a locked-down mode which limits it to a single network interface and IPv6 stack. 6 | 7 | [![See Video](https://img.youtube.com/vi/iLA5oOOlK6o/0.jpg)](https://www.youtube.com/watch?v=iLA5oOOlK6o) 8 | 9 | Watch a video. 10 | 11 | ## Backstory 12 | 13 | We were packaging an appliance with one of the most popular forward proxy solutions out there, Squid, and things were going smoothly. However, we ran into an obstacle that could not be resolved so easily - disabling dns resolution. 14 | We found the answer - that disabling the internal dns is a pre-compile time configuration option. We quickly downloaded the libraries required and built the daemon. This build took quite a long time, and it made us curious. 15 | 16 | That's when we began looking deeper - and we realized that for our use case, Squid and it's > 100,000 lines of code was too much to audit. We didn't need inspection, decryption, and all the other wide varieties of use cases Squid incorporates. 17 | We thought since we already built [delorean](https://github.com/ipv6rslimited/delorean) which already powers the [IPv6rs](https://ipv6.rs) network, why not just build our own using code from there. 18 | 19 | Shrimp only covers 1 of the 100s of brilliant use cases of Squid, but it does it pretty well - it's fast, it's lightweight and staright forward, which makes it easily auditable, just like a shrimp. 20 | 21 | ## Shrimp Features 22 | 23 | Don't let shrimp's size fool you. In Brazilian Jiu Jitsu, when you're on your back, the shrimp is the movement to get out from under your opponent, just like shrimp. 24 | 25 | - Forward Proxy for HTTP and HTTPS (yes, websockets works flawlessly too) 26 | - No decryption/SSL Bumping/etc. 27 | - Can be locked down to a single interface and IPv6 28 | - Supports BASIC authentication, bcrypt hashed 29 | - Utilizes an LRU cache for DNS lookups to improve performance. 30 | - Highly scalable using goroutines. 31 | - Super simple configuration 32 | - IPv4 to ipv6 translation for http://2.2.2.2 type URLs 33 | - **< 700 lines of code so you can read it and see what's going on** 34 | 35 | ## Use Case 36 | 37 | - If you're a VPN provider and need a lightweight forward proxy with bcrypt hashed passwords 38 | - If you're an IPv6rs client and want to make a shared proxy because sharing is caring or plausible deniability 39 | - If you need a lightweight, super fast forward web proxy 40 | 41 | ## Requirements 42 | 43 | - This runs on linux. It may run on mac, but you can just run it with Cloud Seeder on Windows, Mac and Linux. 44 | 45 | ## Configuration 46 | 47 | ``` 48 | { 49 | "listenAddrs": ["[::]:443"], 50 | "plaintextAddr": "0.0.0.0:3128", 51 | "lockdownMode": true, 52 | "ipv6Interface": "wg0", 53 | "dns64Server": "2606:4700:4700::64", 54 | "credentialsFile": "/etc/shrimp/passwd", 55 | "debugMode": false, 56 | "certFile": "cert.pem", 57 | "keyFile": "key.pem", 58 | "dnsCacheCapacity": 100, 59 | "dnsTTL": 300, 60 | "ipv4Translator": "visibleip.com", 61 | "allowedHosts": [".*"], 62 | "disallowedHosts": [ 63 | "^localhost$", 64 | "^127\\.0\\.0\\.1$", 65 | "^10\\.", 66 | "^172\\.(1[6-9]|2[0-9]|3[0-1])\\.", 67 | "^192\\.168\\." 68 | ] 69 | } 70 | ``` 71 | 72 | You can use `visibleip.com` as your ipv4Translator as it is run by IPv6rs and is anycasted across 16 different locations. It's nothing special - just running [legacydns](https://github.com/ipv6rslimited/legacydns) which helps to create 73 | domain names for IP addresses when using IPv6 + NAT64 + dns64. 74 | 75 | ## Lockdown Mode 76 | 77 | Most use cases will have lockdown mode off. This makes it run like a standard, but fast, forward web proxy. 78 | 79 | However, if you're an IPv6rs user, running this in lockdown mode creates intentional benefits: 80 | 81 | - If connecting via IPv4, you will hop thru a logless reverse proxy called [delorean](https://github.com/ipv6rslimited/delorean) running on an IPv6rs router, then to shrimp and then to the internet. 82 | - Your connection will be fully contained to the wg0 interface without any potential leakage. Say goodbye to DNS leaks, IP leaks, etc. 83 | - Some IPv6rs clients may travel and want access to their home IP when browsing, if this is the case just turn lockdown mode off. 84 | 85 | ## Using this 86 | 87 | - FireFox has the FoxyProxy extension which is the easiest way to use shrimp in Firefox since it supports SSL forward web proxies. You don't have to signup for their service to use their extension. 88 | - Curl has a `-x` command line option. 89 | - Chrome doesn't seem to support SSL forward web proxies. Only use in localhost mode. 90 | 91 | ## Tests 92 | 93 | ``` 94 | $ go test -v shrimp.go shrimp_test.go 95 | === RUN TestStripPort 96 | --- PASS: TestStripPort (0.00s) 97 | === RUN TestGetHostPort 98 | --- PASS: TestGetHostPort (0.00s) 99 | === RUN TestIsIPv4Address 100 | --- PASS: TestIsIPv4Address (0.00s) 101 | === RUN TestRemoveAuthHeader 102 | --- PASS: TestRemoveAuthHeader (0.00s) 103 | === RUN TestCheckAuth 104 | --- PASS: TestCheckAuth (0.20s) 105 | === RUN TestConnectTo 106 | --- PASS: TestConnectTo (0.10s) 107 | === RUN TestLookupWithCache 108 | --- PASS: TestLookupWithCache (0.00s) 109 | PASS 110 | ok command-line-arguments 0.303s 111 | ``` 112 | 113 | # License 114 | 115 | Distributed under the COOL License. 116 | 117 | Copyright (c) 2024 IPv6.rs 118 | All Rights Reserved 119 | 120 | -------------------------------------------------------------------------------- /shrimp.go: -------------------------------------------------------------------------------- 1 | /* 2 | ** 3 | ** shrimp 4 | ** A simple forward proxy written in GoLang 5 | ** 6 | ** Distributed under the COOL License. 7 | ** 8 | ** Copyright (c) 2024 IPv6.rs 9 | ** All Rights Reserved 10 | ** 11 | */ 12 | 13 | package main 14 | 15 | import ( 16 | "bufio" 17 | "context" 18 | "crypto/tls" 19 | "encoding/base64" 20 | "encoding/json" 21 | "fmt" 22 | "io" 23 | "log" 24 | "net" 25 | "os" 26 | "regexp" 27 | "strings" 28 | "sync" 29 | "time" 30 | "github.com/ipv6rslimited/lrucache" 31 | "github.com/ipv6rslimited/peter" 32 | "golang.org/x/crypto/bcrypt" 33 | ) 34 | 35 | type Config struct { 36 | ListenAddrs []string `json:"listenAddrs"` 37 | PlaintextAddr string `json:"plaintextAddr"` 38 | LockdownMode bool `json:"lockdownMode"` 39 | IPv6Interface string `json:"ipv6Interface"` 40 | DNS64Server string `json:"dns64Server"` 41 | CredentialsFile string `json:"credentialsFile"` 42 | DebugMode bool `json:"debugMode"` 43 | CertFile string `json:"certFile"` 44 | KeyFile string `json:"keyFile"` 45 | DNSCacheCapacity int `json:"dnsCacheCapacity"` 46 | DNSTTL int `json:"dnsTTL"` 47 | IPv4Translator string `json:"ipv4Translator"` 48 | AllowedHosts []string `json:"allowedHosts"` 49 | DisallowedHosts []string `json:"disallowedHosts"` 50 | } 51 | 52 | type CacheEntry struct { 53 | Address string 54 | Timestamp time.Time 55 | } 56 | 57 | type nullWriter struct{} 58 | 59 | var ( 60 | config Config 61 | users map[string]string 62 | configLock sync.RWMutex 63 | logger *log.Logger 64 | dnsCache *lrucache.LRUCache 65 | systemDNS []string 66 | maxBufferedDataSize = 8192 67 | ) 68 | 69 | 70 | func main() { 71 | loadConfig() 72 | setLogger(config.DebugMode) 73 | loadCredentials() 74 | loadSystemDNS() 75 | 76 | dnsCache = lrucache.NewLRUCache(config.DNSCacheCapacity) 77 | 78 | cert, err := tls.LoadX509KeyPair(config.CertFile, config.KeyFile) 79 | if err != nil { 80 | logger.Fatalf("Error loading certificate: %v", err) 81 | } 82 | 83 | tlsConfig := &tls.Config{ 84 | Certificates: []tls.Certificate{cert}, 85 | MinVersion: tls.VersionTLS12, 86 | } 87 | 88 | listeners := createListeners(tlsConfig) 89 | plaintextListener := createPlaintextListener() 90 | listeners = append(listeners, plaintextListener) 91 | 92 | var wg sync.WaitGroup 93 | for _, listener := range listeners { 94 | wg.Add(1) 95 | go func(l net.Listener) { 96 | defer wg.Done() 97 | for { 98 | conn, err := l.Accept() 99 | if err != nil { 100 | logger.Printf("Error accepting connection: %v", err) 101 | continue 102 | } 103 | go handleConn(conn, l.Addr().String()) 104 | } 105 | }(listener) 106 | } 107 | wg.Wait() 108 | } 109 | 110 | func (nw nullWriter) Write(p []byte) (n int, err error) { 111 | return len(p), nil 112 | } 113 | 114 | func setLogger(enable bool) { 115 | if enable { 116 | logger = log.New(os.Stdout, "", log.LstdFlags) 117 | } else { 118 | logger = log.New(nullWriter{}, "", log.LstdFlags) 119 | } 120 | } 121 | 122 | func loadConfig() { 123 | configLock.Lock() 124 | defer configLock.Unlock() 125 | 126 | file, err := os.Open("shrimp.conf") 127 | if err != nil { 128 | logger.Fatalf("Error opening config file: %v", err) 129 | } 130 | defer file.Close() 131 | 132 | decoder := json.NewDecoder(file) 133 | if err := decoder.Decode(&config); err != nil { 134 | logger.Fatalf("Error decoding config file: %v", err) 135 | } 136 | } 137 | 138 | func loadCredentials() { 139 | configLock.Lock() 140 | defer configLock.Unlock() 141 | 142 | file, err := os.Open(config.CredentialsFile) 143 | if err != nil { 144 | logger.Fatalf("Error opening credentials file: %v", err) 145 | } 146 | defer file.Close() 147 | 148 | users = make(map[string]string) 149 | scanner := bufio.NewScanner(file) 150 | for scanner.Scan() { 151 | parts := strings.SplitN(scanner.Text(), ":", 2) 152 | if len(parts) != 2 { 153 | logger.Fatalf("Invalid credentials format") 154 | } 155 | users[parts[0]] = parts[1] 156 | } 157 | 158 | if err := scanner.Err(); err != nil { 159 | logger.Fatalf("Error reading credentials file: %v", err) 160 | } 161 | } 162 | 163 | func loadSystemDNS() { 164 | file, err := os.Open("/etc/resolv.conf") 165 | if err != nil { 166 | logger.Fatalf("Error opening /etc/resolv.conf: %v", err) 167 | } 168 | defer file.Close() 169 | 170 | scanner := bufio.NewScanner(file) 171 | for scanner.Scan() { 172 | line := scanner.Text() 173 | if strings.HasPrefix(line, "nameserver") { 174 | parts := strings.Fields(line) 175 | if len(parts) > 1 { 176 | systemDNS = append(systemDNS, parts[1]+":53") 177 | } 178 | } 179 | } 180 | 181 | if err := scanner.Err(); err != nil { 182 | logger.Fatalf("Error reading /etc/resolv.conf: %v", err) 183 | } 184 | } 185 | 186 | func createListeners(tlsConfig *tls.Config) []net.Listener { 187 | var listeners []net.Listener 188 | for _, addr := range config.ListenAddrs { 189 | listener, err := tls.Listen("tcp", addr, tlsConfig) 190 | if err != nil { 191 | logger.Fatalf("Error starting proxy on %s: %v", addr, err) 192 | } 193 | listeners = append(listeners, listener) 194 | logger.Printf("Proxy listening on %s", addr) 195 | } 196 | return listeners 197 | } 198 | 199 | func createPlaintextListener() net.Listener { 200 | listener, err := net.Listen("tcp", config.PlaintextAddr) 201 | if err != nil { 202 | logger.Fatalf("Error starting plaintext proxy on %s: %v", config.PlaintextAddr, err) 203 | } 204 | logger.Printf("Plaintext proxy listening on %s", config.PlaintextAddr) 205 | return listener 206 | } 207 | 208 | func handleConn(conn net.Conn, addr string) { 209 | defer conn.Close() 210 | 211 | reader := bufio.NewReader(conn) 212 | host, bufferedData, err := getNameAndBufferFromHTTPConnection(reader) 213 | if err != nil { 214 | logger.Printf("Error extracting host: %v", err) 215 | sendErrorResponse(conn, 400, "Invalid Request") 216 | return 217 | } 218 | 219 | if !isHostAllowed(host) { 220 | logger.Printf("Host %s is not allowed", host) 221 | sendErrorResponse(conn, 403, "Access Denied.") 222 | return 223 | } 224 | 225 | auth := extractAuth(bufferedData) 226 | authenticated, promptPassword := checkAuth(auth) 227 | if !authenticated { 228 | if promptPassword { 229 | sendErrorResponse(conn, 407, "Proxy Authentication Required. Please provide credentials.") 230 | } else { 231 | sendErrorResponse(conn, 403, "Access Denied.") 232 | } 233 | return 234 | } 235 | 236 | if isWebSocketUpgrade(bufferedData) { 237 | handleWebSocket(conn, host, bufferedData) 238 | return 239 | } 240 | 241 | if isConnectMethod(bufferedData) { 242 | handleHTTPS(conn, host) 243 | } else { 244 | handleHTTP(conn, host, bufferedData) 245 | } 246 | } 247 | 248 | func getNameAndBufferFromHTTPConnection(reader *bufio.Reader) (string, []byte, error) { 249 | logger.Println("Extracting name from HTTP connection") 250 | bufferedData := make([]byte, 0, maxBufferedDataSize) 251 | var host string 252 | 253 | for { 254 | line, err := reader.ReadString('\n') 255 | if err != nil { 256 | if err == io.EOF { 257 | break 258 | } 259 | return "", nil, fmt.Errorf("failed to read line: %w", err) 260 | } 261 | 262 | bufferedData = append(bufferedData, []byte(line)...) 263 | if len(bufferedData) > maxBufferedDataSize { 264 | return "", nil, fmt.Errorf("buffered data exceeds maximum size") 265 | } 266 | 267 | line = strings.TrimRight(line, "\r\n") 268 | 269 | if strings.HasPrefix(strings.ToLower(line), "host: ") { 270 | host = strings.TrimSpace(line[6:]) 271 | host = stripPort(host) 272 | logger.Printf("Extracted host from HTTP: %s", host) 273 | } 274 | 275 | if len(line) == 0 { 276 | break 277 | } 278 | } 279 | 280 | if host == "" { 281 | return "", nil, fmt.Errorf("host header not found") 282 | } 283 | 284 | remainingData := make([]byte, reader.Buffered()) 285 | n, err := reader.Read(remainingData) 286 | if err != nil && err != io.EOF { 287 | return "", nil, fmt.Errorf("failed to read remaining data: %w", err) 288 | } 289 | 290 | if len(bufferedData)+n > maxBufferedDataSize { 291 | return "", nil, fmt.Errorf("remaining data exceeds maximum buffer size") 292 | } 293 | bufferedData = append(bufferedData, remainingData[:n]...) 294 | 295 | return host, bufferedData, nil 296 | } 297 | 298 | func stripPort(host string) string { 299 | if strings.HasPrefix(host, "[") { 300 | endIndex := strings.Index(host, "]") 301 | if endIndex == -1 { 302 | return host 303 | } 304 | if endIndex+1 < len(host) && host[endIndex+1] == ':' { 305 | return host[:endIndex+1] 306 | } 307 | return host 308 | } 309 | if colonIndex := strings.LastIndex(host, ":"); colonIndex != -1 { 310 | return host[:colonIndex] 311 | } 312 | return host 313 | } 314 | 315 | func extractAuth(data []byte) string { 316 | lines := strings.Split(string(data), "\r\n") 317 | for _, line := range lines { 318 | if strings.HasPrefix(strings.ToLower(line), "proxy-authorization: ") { 319 | return strings.TrimSpace(line[len("proxy-authorization: "):]) 320 | } 321 | } 322 | return "" 323 | } 324 | 325 | func sendErrorResponse(conn net.Conn, statusCode int, message string) { 326 | response := fmt.Sprintf("HTTP/1.1 %d %s\r\nProxy-Authenticate: Basic realm=\"Access to the proxy\"\r\nContent-Length: %d\r\nContent-Type: text/plain\r\n\r\n%s", statusCode, getStatusText(statusCode), len(message), message) 327 | conn.Write([]byte(response)) 328 | } 329 | 330 | func getStatusText(statusCode int) string { 331 | switch statusCode { 332 | case 400: 333 | return "Bad Request" 334 | case 403: 335 | return "Forbidden" 336 | case 407: 337 | return "Proxy Authentication Required" 338 | case 503: 339 | return "Service Unavailable" 340 | default: 341 | return "Unknown Status" 342 | } 343 | } 344 | 345 | func handleHTTPS(conn net.Conn, host string) { 346 | logger.Printf("Handling CONNECT method for host: %s", host) 347 | 348 | host, port := getHostPort(host, "443") 349 | 350 | destConn, err := connectTo(host, port) 351 | if err != nil { 352 | logger.Printf("Error connecting to host %s: %v", host, err) 353 | sendErrorResponse(conn, 503, "Service Unavailable") 354 | return 355 | } 356 | defer destConn.Close() 357 | 358 | conn.Write([]byte("HTTP/1.1 200 Connection Established\r\n\r\n")) 359 | 360 | p := peter.NewPeter(conn, destConn) 361 | p.Start() 362 | logger.Printf("HTTPS connection closed for host: %s", host) 363 | } 364 | 365 | func handleHTTP(conn net.Conn, host string, bufferedData []byte) { 366 | logger.Printf("Handling HTTP request for host: %s", host) 367 | 368 | host, port := getHostPort(host, "80") 369 | 370 | destConn, err := connectTo(host, port) 371 | if err != nil { 372 | logger.Printf("Error connecting to host %s: %v", host, err) 373 | sendErrorResponse(conn, 503, "Service Unavailable") 374 | return 375 | } 376 | defer destConn.Close() 377 | 378 | forwardedData := removeAuthHeader(bufferedData) 379 | 380 | _, err = destConn.Write(forwardedData) 381 | if err != nil { 382 | logger.Printf("Error writing request to destination: %v", err) 383 | return 384 | } 385 | 386 | p := peter.NewPeter(conn, destConn) 387 | p.Start() 388 | logger.Printf("HTTP connection closed for host: %s", host) 389 | } 390 | 391 | func handleWebSocket(conn net.Conn, host string, bufferedData []byte) { 392 | logger.Printf("Handling WebSocket upgrade for host: %s", host) 393 | 394 | host, port := getHostPort(host, "80") 395 | 396 | destConn, err := connectTo(host, port) 397 | if err != nil { 398 | logger.Printf("Error connecting to host %s: %v", host, err) 399 | sendErrorResponse(conn, 503, "Service Unavailable") 400 | return 401 | } 402 | defer destConn.Close() 403 | 404 | forwardedData := removeAuthHeader(bufferedData) 405 | 406 | _, err = destConn.Write(forwardedData) 407 | if err != nil { 408 | logger.Printf("Error writing WebSocket upgrade request: %v", err) 409 | return 410 | } 411 | 412 | p := peter.NewPeter(conn, destConn) 413 | p.Start() 414 | logger.Printf("WebSocket connection closed for host: %s", host) 415 | } 416 | 417 | func getHostPort(hostport, defaultPort string) (string, string) { 418 | if strings.HasPrefix(hostport, "[") { 419 | endIdx := strings.Index(hostport, "]") 420 | if endIdx == -1 { 421 | return hostport, defaultPort 422 | } 423 | 424 | if len(hostport) > endIdx+2 && hostport[endIdx+1] == ':' { 425 | return hostport[:endIdx+1], hostport[endIdx+2:] 426 | } 427 | return hostport[:endIdx+1], defaultPort 428 | } 429 | 430 | parts := strings.Split(hostport, ":") 431 | if len(parts) == 2 && parts[1] != "" { 432 | return parts[0], parts[1] 433 | } else if len(parts) > 2 { 434 | return strings.Join(parts[:len(parts)-1], ":"), parts[len(parts)-1] 435 | } 436 | return strings.TrimSuffix(hostport, ":"), defaultPort 437 | } 438 | 439 | func checkAuth(authHeader string) (bool, bool) { 440 | if authHeader == "" { 441 | return false, true 442 | } 443 | 444 | parts := strings.SplitN(authHeader, " ", 2) 445 | if len(parts) != 2 || parts[0] != "Basic" { 446 | return false, true 447 | } 448 | 449 | auth, err := base64.StdEncoding.DecodeString(parts[1]) 450 | if err != nil { 451 | return false, true 452 | } 453 | 454 | creds := strings.SplitN(string(auth), ":", 2) 455 | if len(creds) != 2 { 456 | return false, true 457 | } 458 | 459 | configLock.RLock() 460 | hashedPassword, ok := users[creds[0]] 461 | configLock.RUnlock() 462 | if !ok { 463 | return false, true 464 | } 465 | 466 | err = bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(creds[1])) 467 | if err != nil { 468 | return false, true 469 | } 470 | 471 | return true, false 472 | } 473 | 474 | func connectTo(host, port string) (net.Conn, error) { 475 | host = strings.Trim(host, "[]") 476 | if net.ParseIP(host) != nil { 477 | if config.LockdownMode && isIPv4Address(host) { 478 | host = fmt.Sprintf("%s.%s", host, config.IPv4Translator) 479 | } else { 480 | return connectDo(fmt.Sprintf("[%s]",host), host, port) 481 | } 482 | } 483 | 484 | ip, err := lookupWithCache(host) 485 | if err != nil { 486 | return nil, err 487 | } 488 | 489 | if !isIPAllowed(ip) { 490 | return nil, fmt.Errorf("IP %s is not allowed", ip) 491 | } 492 | 493 | return connectDo(host, ip, port) 494 | } 495 | 496 | func connectDo(host, ip, port string) (net.Conn, error) { 497 | d := &net.Dialer{ 498 | Timeout: 10 * time.Second, 499 | DualStack: !config.LockdownMode, 500 | } 501 | 502 | if config.LockdownMode { 503 | addr, err := getIPv6Addr(config.IPv6Interface) 504 | if err != nil { 505 | return nil, err 506 | } 507 | d.LocalAddr = addr 508 | } 509 | 510 | logger.Printf("Connecting to %s:%s (resolved IP: %s)", host, port, ip) 511 | 512 | return d.Dial("tcp", net.JoinHostPort(ip, port)) 513 | } 514 | 515 | func getIPv6Addr(ifaceName string) (net.Addr, error) { 516 | iface, err := net.InterfaceByName(ifaceName) 517 | if err != nil { 518 | return nil, err 519 | } 520 | 521 | addrs, err := iface.Addrs() 522 | if err != nil { 523 | return nil, err 524 | } 525 | 526 | for _, addr := range addrs { 527 | if ipNet, ok := addr.(*net.IPNet); ok && ipNet.IP.To4() == nil { 528 | return &net.TCPAddr{IP: ipNet.IP}, nil 529 | } 530 | } 531 | 532 | return nil, fmt.Errorf("no IPv6 address found for interface %s", ifaceName) 533 | } 534 | 535 | func lookupWithCache(hostname string) (string, error) { 536 | logger.Printf("Looking up hostname: %s", hostname) 537 | if entry, found := dnsCache.Get(hostname); found { 538 | cacheEntry := entry.(CacheEntry) 539 | if time.Since(cacheEntry.Timestamp) < time.Duration(config.DNSTTL)*time.Second { 540 | logger.Printf("Cache hit for hostname: %s, address: %s", hostname, cacheEntry.Address) 541 | return cacheEntry.Address, nil 542 | } 543 | go func() { 544 | if ip, err := lookupRaw(hostname); err == nil { 545 | dnsCache.Put(hostname, CacheEntry{Address: ip, Timestamp: time.Now()}) 546 | } 547 | }() 548 | logger.Printf("Cache stale for hostname: %s, using old address while refreshing", hostname) 549 | return cacheEntry.Address, nil 550 | } 551 | logger.Printf("Cache miss for hostname: %s", hostname) 552 | return lookupRaw(hostname) 553 | } 554 | 555 | func lookupRaw(hostname string) (string, error) { 556 | logger.Printf("Performing raw lookup for hostname: %s", hostname) 557 | var ip string 558 | var err error 559 | 560 | if config.LockdownMode { 561 | ip, err = resolveDNS64(hostname) 562 | } else { 563 | ip, err = resolveDefaultDNS(hostname) 564 | } 565 | 566 | if err != nil { 567 | logger.Printf("Raw lookup failed for hostname: %s, error: %v", hostname, err) 568 | return "", err 569 | } 570 | 571 | dnsCache.Put(hostname, CacheEntry{Address: ip, Timestamp: time.Now()}) 572 | logger.Printf("Raw lookup succeeded for hostname: %s, address: %s", hostname, ip) 573 | return ip, nil 574 | } 575 | 576 | func resolveDNS64(host string) (string, error) { 577 | resolver := &net.Resolver{ 578 | Dial: func(ctx context.Context, network, address string) (net.Conn, error) { 579 | dnsServerAddress := fmt.Sprintf("[%s]:53", config.DNS64Server) 580 | return net.Dial("udp", dnsServerAddress) 581 | }, 582 | } 583 | 584 | addrs, err := resolver.LookupHost(context.Background(), host) 585 | if err != nil { 586 | return "", err 587 | } 588 | 589 | for _, addr := range addrs { 590 | if strings.Contains(addr, ":") { 591 | logger.Printf("Resolved IPv6 address for host %s: %s", host, addr) 592 | return addr, nil 593 | } 594 | } 595 | 596 | return "", fmt.Errorf("no IPv6 address found for host %s", host) 597 | } 598 | 599 | func resolveDefaultDNS(host string) (string, error) { 600 | addrs, err := net.LookupHost(host) 601 | if err != nil { 602 | return "", err 603 | } 604 | 605 | for _, addr := range addrs { 606 | if strings.Contains(addr, ":") { 607 | logger.Printf("Resolved IPv6 address for host %s: %s", host, addr) 608 | return addr, nil 609 | } 610 | if !config.LockdownMode { 611 | logger.Printf("Resolved IPv4 address for host %s: %s", host, addr) 612 | return addr, nil 613 | } 614 | } 615 | 616 | return "", fmt.Errorf("no valid address found for host %s", host) 617 | } 618 | 619 | func removeAuthHeader(data []byte) []byte { 620 | lines := strings.Split(string(data), "\r\n") 621 | filteredLines := []string{} 622 | for _, line := range lines { 623 | if !strings.HasPrefix(strings.ToLower(line), "proxy-authorization: ") { 624 | filteredLines = append(filteredLines, line) 625 | } 626 | } 627 | return []byte(strings.Join(filteredLines, "\r\n")) 628 | } 629 | 630 | func isIPv4Address(host string) bool { 631 | ipv4Regex := regexp.MustCompile(`^(\d{1,3}\.){3}\d{1,3}$`) 632 | return ipv4Regex.MatchString(host) 633 | } 634 | 635 | func isHostAllowed(host string) bool { 636 | for _, pattern := range config.DisallowedHosts { 637 | matched, _ := regexp.MatchString(pattern, host) 638 | if matched { 639 | logger.Printf("Host %s is disallowed by pattern %s", host, pattern) 640 | return false 641 | } 642 | } 643 | for _, pattern := range config.AllowedHosts { 644 | matched, _ := regexp.MatchString(pattern, host) 645 | if matched { 646 | logger.Printf("Host %s is allowed by pattern %s", host, pattern) 647 | return true 648 | } 649 | } 650 | logger.Printf("Host %s is not explicitly allowed or disallowed, defaulting to disallowed", host) 651 | return false 652 | } 653 | 654 | func isWebSocketUpgrade(data []byte) bool { 655 | dataString := strings.ToLower(string(data)) 656 | return strings.Contains(dataString, "connection: upgrade") && strings.Contains(dataString, "upgrade: websocket") 657 | } 658 | 659 | func isConnectMethod(data []byte) bool { 660 | dataString := strings.ToUpper(string(data)) 661 | return strings.HasPrefix(dataString, "CONNECT ") 662 | } 663 | 664 | func isIPAllowed(ip string) bool { 665 | translatedIP := ipv6ToIPv4(ip) 666 | 667 | for _, pattern := range config.DisallowedHosts { 668 | matched, _ := regexp.MatchString(pattern, ip) 669 | if matched { 670 | logger.Printf("IP %s is disallowed by pattern %s", ip, pattern) 671 | return false 672 | } 673 | if translatedIP != ip { 674 | matched, _ := regexp.MatchString(pattern, translatedIP) 675 | if matched { 676 | logger.Printf("Translated IP %s (original %s) is disallowed by pattern %s", translatedIP, ip, pattern) 677 | return false 678 | } 679 | } 680 | } 681 | return true 682 | } 683 | 684 | func ipv6ToIPv4(ipv6 string) string { 685 | const prefix = "64:ff9b::" 686 | if strings.HasPrefix(ipv6, prefix) { 687 | ipv4Part := ipv6[len(prefix):] 688 | ipv4Bytes := net.ParseIP(ipv4Part).To4() 689 | if ipv4Bytes != nil { 690 | return ipv4Bytes.String() 691 | } 692 | } 693 | return ipv6 694 | } 695 | --------------------------------------------------------------------------------