├── .gitignore ├── LICENSE ├── README.md ├── assets ├── LightProxy-Bridge.png └── LightProxy-Relay.png ├── cmd ├── bridge.go ├── config.go ├── config_common.go ├── config_darwin.go ├── config_linux.go ├── config_windows.go ├── relay.go ├── root.go └── tool.go ├── go.mod ├── go.sum └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode/ 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 GetcharZp 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LightProxy 2 | 3 | + **LightProxy** 提供了桥接,代理上网等功能。适用于在同一局域网内,设备A能上网,设备B不能上网的情况(此时,设备B可以通过LightProxy借助设备A上网)。 4 | + 特别是工厂的环境,很多检测服务器为了安全是不能上网的,只有用于远程的 PC 能上网,此时通过 **LightProxy** 可以将 PC 的代理服务桥接到设备B上,让设备B也能临时上网。 5 | 6 | ## relay (代理上网) 7 | 8 | + 流程图: 9 | 10 | ![LightProxy-Relay.png](assets/LightProxy-Relay.png) 11 | 12 | + 命令: 13 | 14 | ```shell 15 | # 1. 代理转发(默认端口:8080) 16 | proxy relay 17 | 18 | # 2. 代理转发,指定端口 19 | proxy relay --port 8000 20 | ``` 21 | 22 | ## bridge (桥接转发) 23 | 24 | + 流程图: 25 | 26 | ![LightProxy-Bridge.png](assets/LightProxy-Bridge.png) 27 | 28 | + 命令: 29 | 30 | ```shell 31 | # 1. 桥接转发(默认端口:8080) 32 | proxy bridge --relay 192.168.1.3:8080 33 | 34 | # 2. 桥接转发,指定端口 35 | proxy bridge --relay 192.168.1.3:8080 --port 8000 36 | 37 | # PS:桥接转发 Clash 的网络 38 | proxy bridge --relay 127.0.0.1:7890 39 | ``` 40 | 41 | ## config (配置管理) 42 | 43 | 说明:如果需要在终端中使用配置的代理地址,需要打开新的终端 44 | 45 | ```shell 46 | # 1. 清除配置 47 | sudo proxy config --set 0 48 | 49 | # 2. 设置配置(set 后面的参数为 [relay 所在服务器的IP]:[relay 指定的端口]) 50 | sudo proxy config --set http://192.168.1.3:8080 51 | ``` 52 | -------------------------------------------------------------------------------- /assets/LightProxy-Bridge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetcharZp/light-proxy/ddbd6dc249e37d3d04645b6e468b54791ee75226/assets/LightProxy-Bridge.png -------------------------------------------------------------------------------- /assets/LightProxy-Relay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetcharZp/light-proxy/ddbd6dc249e37d3d04645b6e468b54791ee75226/assets/LightProxy-Relay.png -------------------------------------------------------------------------------- /cmd/bridge.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "log" 6 | "net" 7 | "time" 8 | ) 9 | 10 | var ( 11 | bridgePort string 12 | relayAddress string 13 | ) 14 | 15 | func NewBridgeCommand() *cobra.Command { 16 | cmd := &cobra.Command{ 17 | Use: "bridge", 18 | Short: "Use bridge CLI to forward requests to the relay PC.", 19 | Run: func(cmd *cobra.Command, args []string) { 20 | bridgePort, _ = cmd.Flags().GetString("port") 21 | relayAddress, _ = cmd.Flags().GetString("relay") 22 | bridge() 23 | }, 24 | } 25 | 26 | cmd.Flags().StringP("port", "p", "8080", "proxy port, default 8080") 27 | cmd.Flags().StringP("relay", "r", "", "relay address (required)") 28 | cmd.MarkFlagRequired("relay") 29 | 30 | return cmd 31 | } 32 | 33 | func bridge() { 34 | ln, err := net.Listen("tcp", ":"+bridgePort) 35 | if err != nil { 36 | log.Printf("[sys] listen error:%s \n", err.Error()) 37 | return 38 | } 39 | showLocalIpv4s() 40 | log.Printf("[sys] proxy server start success port:%s \n", bridgePort) 41 | 42 | for { 43 | clientConn, err := ln.Accept() 44 | if err != nil { 45 | log.Printf("[sys] accept error:%s \n", err.Error()) 46 | continue 47 | } 48 | go bridgePipe(clientConn) 49 | } 50 | } 51 | 52 | func bridgePipe(clientConn net.Conn) { 53 | log.Printf("[sys] get proxy request address: %s\n", clientConn.RemoteAddr().String()) 54 | serverConn, err := net.DialTimeout("tcp", relayAddress, 10*time.Second) 55 | if err != nil { 56 | log.Printf("[sys] net dial error:%s \n", err.Error()) 57 | return 58 | } 59 | 60 | go transfer(serverConn, clientConn) 61 | go transfer(clientConn, serverConn) 62 | } 63 | -------------------------------------------------------------------------------- /cmd/config.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "runtime" 6 | ) 7 | 8 | var supportOS = map[string]struct{}{ 9 | "linux": {}, 10 | "windows": {}, 11 | "darwin": {}, 12 | } 13 | 14 | func NewConfigCommand() *cobra.Command { 15 | cmd := &cobra.Command{ 16 | Use: "config", 17 | Short: "Use config CLI to configure proxy settings.", 18 | Run: func(cmd *cobra.Command, args []string) { 19 | set, _ := cmd.Flags().GetString("set") 20 | config(set) 21 | }, 22 | } 23 | 24 | cmd.Flags().StringP("set", "s", "0", ` 25 | --set 0 -> use 0 to clear proxy config 26 | --set 127.0.0.1:8080 -> with domain param to set proxy config 27 | `) 28 | 29 | return cmd 30 | } 31 | 32 | // config 代理配置 33 | // 34 | // set: 0 , 清除配置的代理 35 | // set: 127.0.0.1:8080, 设置代理地址 36 | func config(set string) { 37 | if _, ok := supportOS[runtime.GOOS]; !ok { 38 | println("auto config not support for os:" + runtime.GOOS) 39 | return 40 | } 41 | if set == "0" { 42 | configClear() 43 | } else { 44 | configClear() 45 | configSet(set) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /cmd/config_common.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || linux 2 | 3 | package cmd 4 | 5 | import ( 6 | "fmt" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | func configClear() { 12 | if err := removeFromEnv("http_proxy"); err != nil { 13 | println("[sys] proxy config clear failed", err.Error()) 14 | return 15 | } 16 | if err := removeFromEnv("https_proxy"); err != nil { 17 | println("[sys] proxy config clear failed", err.Error()) 18 | return 19 | } 20 | println("[sys] proxy config clear successfully") 21 | } 22 | 23 | func configSet(domain string) { 24 | if err := appendToEnv("http_proxy", domain); err != nil { 25 | println("[sys] proxy config set failed", err.Error()) 26 | return 27 | } 28 | if err := appendToEnv("https_proxy", domain); err != nil { 29 | println("[sys] proxy config set failed", err.Error()) 30 | return 31 | } 32 | println("[sys] proxy config set successfully") 33 | } 34 | 35 | func appendToEnv(variable, value string) error { 36 | configPath, err := expandPath(shellConfigPath) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | file, err := os.OpenFile(configPath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644) 42 | if err != nil { 43 | return err 44 | } 45 | defer file.Close() 46 | 47 | _, err = file.WriteString(fmt.Sprintf("\nexport %s=\"%s\"", variable, value)) 48 | return err 49 | } 50 | 51 | func removeFromEnv(variable string) error { 52 | configPath, err := expandPath(shellConfigPath) 53 | if err != nil { 54 | return err 55 | } 56 | 57 | content, err := os.ReadFile(configPath) 58 | if err != nil { 59 | return fmt.Errorf("failed to read %s: %w", configPath, err) 60 | } 61 | lines := strings.Split(string(content), "\n") 62 | 63 | var newLines []string 64 | for _, line := range lines { 65 | if strings.HasPrefix(line, "export "+variable+"=") { 66 | continue 67 | } 68 | newLines = append(newLines, line) 69 | } 70 | 71 | newContent := strings.Join(newLines, "\n") 72 | err = os.WriteFile(configPath, []byte(newContent), 0644) 73 | if err != nil { 74 | return fmt.Errorf("failed to write to %s: %w", configPath, err) 75 | } 76 | 77 | return nil 78 | } 79 | 80 | func expandPath(path string) (string, error) { 81 | home, err := os.UserHomeDir() 82 | if err != nil { 83 | return "", err 84 | } 85 | return strings.Replace(path, "~", home, 1), nil 86 | } 87 | -------------------------------------------------------------------------------- /cmd/config_darwin.go: -------------------------------------------------------------------------------- 1 | //go:build darwin 2 | 3 | package cmd 4 | 5 | const shellConfigPath = "~/.zshrc" // 适用于大多数 macOS 用户,若使用 bash 可改为 ~/.bash_profile 6 | -------------------------------------------------------------------------------- /cmd/config_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | package cmd 4 | 5 | const shellConfigPath = "/etc/environment" 6 | -------------------------------------------------------------------------------- /cmd/config_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package cmd 4 | 5 | import ( 6 | "github.com/up-zero/gotool/sysutil" 7 | "golang.org/x/sys/windows/registry" 8 | "log" 9 | "net/url" 10 | ) 11 | 12 | func configClear() { 13 | // 代理 14 | key, err := registry.OpenKey(registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\Internet Settings`, registry.SET_VALUE) 15 | if err != nil { 16 | log.Fatalf("[sys] registry open error:%s \n", err.Error()) 17 | } 18 | defer key.Close() 19 | if err = key.SetDWordValue("ProxyEnable", 0x0); err != nil { 20 | log.Fatalf("[sys] registry set error:%s \n", err.Error()) 21 | } 22 | 23 | // 环境变量 24 | if err = sysutil.ExecCommand("setx", "HTTP_PROXY", ""); err != nil { 25 | log.Fatalf("[sys] exec command for http_proxy error:%s \n", err.Error()) 26 | } 27 | if err = sysutil.ExecCommand("setx", "HTTPS_PROXY", ""); err != nil { 28 | log.Fatalf("[sys] exec command for https_proxy error:%s \n", err.Error()) 29 | } 30 | 31 | println("[sys] proxy config clear successfully") 32 | } 33 | 34 | func configSet(domain string) { 35 | host := "" 36 | u, err := url.Parse(domain) 37 | if err != nil { 38 | host = domain 39 | } else { 40 | host = u.Host 41 | } 42 | 43 | // 代理 44 | proxyKey, err := registry.OpenKey(registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\Internet Settings`, registry.SET_VALUE) 45 | if err != nil { 46 | log.Fatalf("[sys] registry open error:%s \n", err.Error()) 47 | } 48 | defer proxyKey.Close() 49 | if err = proxyKey.SetDWordValue("ProxyEnable", 0x1); err != nil { 50 | log.Fatalf("[sys] registry set proxy enable error:%s \n", err.Error()) 51 | } 52 | if err = proxyKey.SetStringValue("ProxyServer", host); err != nil { 53 | log.Fatalf("[sys] registry set proxy server error:%s \n", err.Error()) 54 | } 55 | if err = proxyKey.SetStringValue("ProxyOverride", `localhost;127.*;10.*;172.16.*;172.17.*;172.18.*;172.19.*;172.20.*;172.21.*;172.22.*;172.23.*;172.24.*;172.25.*;172.26.*;172.27.*;172.28.*;172.29.*;172.30.*;172.31.*;192.168.*;127.0.0.1;`); err != nil { 56 | log.Fatalf("[sys] registry set proxy override error:%s \n", err.Error()) 57 | } 58 | 59 | // 环境变量 60 | if err = sysutil.ExecCommand("setx", "HTTP_PROXY", host); err != nil { 61 | log.Fatalf("[sys] exec command for http_proxy error:%s \n", err.Error()) 62 | } 63 | if err = sysutil.ExecCommand("setx", "HTTPS_PROXY", host); err != nil { 64 | log.Fatalf("[sys] exec command for https_proxy error:%s \n", err.Error()) 65 | } 66 | 67 | println("[sys] proxy config set successfully") 68 | } 69 | -------------------------------------------------------------------------------- /cmd/relay.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "io" 6 | "log" 7 | "net" 8 | "net/http" 9 | "time" 10 | ) 11 | 12 | func NewRelayCommand() *cobra.Command { 13 | cmd := &cobra.Command{ 14 | Use: "relay", 15 | Short: "Use relay CLI to forward requests to the real network.", 16 | Run: func(cmd *cobra.Command, args []string) { 17 | port, _ := cmd.Flags().GetString("port") 18 | relay(port) 19 | }, 20 | } 21 | 22 | cmd.Flags().StringP("port", "p", "8080", "proxy port, default 8080") 23 | 24 | return cmd 25 | } 26 | 27 | func relay(port string) { 28 | server := &http.Server{ 29 | Addr: ":" + port, 30 | Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 31 | if r.Method == http.MethodConnect { 32 | handleHttps(w, r) 33 | } else { 34 | handleHttp(w, r) 35 | } 36 | }), 37 | } 38 | 39 | showLocalIpv4s() 40 | log.Printf("[sys] proxy server start success port:%s \n", port) 41 | log.Fatal(server.ListenAndServe()) 42 | } 43 | 44 | func handleHttp(w http.ResponseWriter, r *http.Request) { 45 | transport := &http.Transport{} 46 | log.Printf("[sys] get proxy request address: %s \n", r.URL.String()) 47 | outReq, err := http.NewRequest(r.Method, r.URL.String(), r.Body) 48 | if err != nil { 49 | log.Printf("[sys] create request error:%s \n", err.Error()) 50 | http.Error(w, "[http] failed to create request", http.StatusInternalServerError) 51 | return 52 | } 53 | 54 | outReq.Header = r.Header 55 | resp, err := transport.RoundTrip(outReq) 56 | if err != nil { 57 | log.Printf("[sys] transport round trip error:%s \n", err.Error()) 58 | http.Error(w, "[http] transport round trip error:"+err.Error(), http.StatusInternalServerError) 59 | return 60 | } 61 | defer resp.Body.Close() 62 | 63 | for k, v := range resp.Header { 64 | w.Header()[k] = v 65 | } 66 | w.WriteHeader(resp.StatusCode) 67 | io.Copy(w, resp.Body) 68 | } 69 | 70 | func handleHttps(w http.ResponseWriter, r *http.Request) { 71 | log.Printf("[sys] get proxy request address: %s \n", r.Host) 72 | destConn, err := net.DialTimeout("tcp", r.Host, 10*time.Second) 73 | if err != nil { 74 | log.Printf("[sys] dial timeout error:%s \n", err.Error()) 75 | http.Error(w, "[net] dial timeout error:"+err.Error(), http.StatusServiceUnavailable) 76 | return 77 | } 78 | w.WriteHeader(http.StatusOK) 79 | hijacker, ok := w.(http.Hijacker) 80 | if !ok { 81 | log.Printf("[sys] hijacker not supported \n") 82 | http.Error(w, "[http] hijacking not supported", http.StatusInternalServerError) 83 | return 84 | } 85 | clientConn, _, err := hijacker.Hijack() 86 | if err != nil { 87 | log.Printf("[sys] hijack error:%s \n", err.Error()) 88 | http.Error(w, "[http] hijack error:"+err.Error(), http.StatusServiceUnavailable) 89 | } 90 | go transfer(destConn, clientConn) 91 | go transfer(clientConn, destConn) 92 | } 93 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "log" 6 | ) 7 | 8 | func NewRootCommand() *cobra.Command { 9 | cmd := &cobra.Command{ 10 | Use: "proxy", 11 | Short: "A light proxy CLI tool", 12 | Long: `This is a light proxy CLI tool developed by GetcharZp. It is primarily used for proxying internet connections, providing an easy way to manage and configure proxy settings.`, 13 | Run: func(cmd *cobra.Command, args []string) { 14 | if err := cmd.Help(); err != nil { 15 | log.Fatalf("[sys] run cmd help error:%s \n", err.Error()) 16 | } 17 | }, 18 | } 19 | 20 | // proxy 直连 21 | cmd.AddCommand(NewRelayCommand()) 22 | // proxy 桥接 23 | cmd.AddCommand(NewBridgeCommand()) 24 | // 配置 25 | cmd.AddCommand(NewConfigCommand()) 26 | 27 | return cmd 28 | } 29 | -------------------------------------------------------------------------------- /cmd/tool.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/up-zero/gotool/netutil" 5 | "io" 6 | "log" 7 | "strings" 8 | ) 9 | 10 | func transfer(destination io.WriteCloser, source io.ReadCloser) { 11 | defer destination.Close() 12 | defer source.Close() 13 | io.Copy(destination, source) 14 | } 15 | 16 | func showLocalIpv4s() { 17 | ips, err := netutil.Ipv4sLocal() 18 | if err == nil { 19 | log.Printf("[sys] local ipv4: %s \n", strings.Join(ips, ";")) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/getcharzp/light-proxy 2 | 3 | go 1.22.7 4 | 5 | require ( 6 | github.com/spf13/cobra v1.8.1 7 | golang.org/x/sys v0.29.0 8 | ) 9 | 10 | require ( 11 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 12 | github.com/spf13/pflag v1.0.5 // indirect 13 | github.com/up-zero/gotool v0.0.0-20250424084133-5ca462645a53 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 2 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 3 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 4 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 5 | github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= 6 | github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= 7 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 8 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 9 | github.com/up-zero/gotool v0.0.0-20250123075653-fb4857af3205 h1:JurBQRvMFnSohCvlsqcY4J9CtkoMjc5OTl05iqwsxjE= 10 | github.com/up-zero/gotool v0.0.0-20250123075653-fb4857af3205/go.mod h1:+jwIpLHojqHUvbEmNXv/F5acdHSEkJIbNXRqT1IE78I= 11 | github.com/up-zero/gotool v0.0.0-20250424084133-5ca462645a53 h1:aABp1nELuFKSdPVdFKdvjIpq08OmVDaCR+bLx8PS7hw= 12 | github.com/up-zero/gotool v0.0.0-20250424084133-5ca462645a53/go.mod h1:+jwIpLHojqHUvbEmNXv/F5acdHSEkJIbNXRqT1IE78I= 13 | golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= 14 | golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 15 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 16 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 17 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/getcharzp/light-proxy/cmd" 5 | "log" 6 | ) 7 | 8 | func main() { 9 | rootCmd := cmd.NewRootCommand() 10 | 11 | if err := rootCmd.Execute(); err != nil { 12 | log.Fatal(err) 13 | } 14 | } 15 | --------------------------------------------------------------------------------