diff --git a/README.md b/README.md index 49df588..fbf8c7d 100644 --- a/README.md +++ b/README.md @@ -228,6 +228,8 @@ $ docker compose up -d + `shadowsocks-url`: Shadowsocks 服务端 URL。例如:`ss://aes-128-gcm:password@server:port`。格式[参考此处](https://github.com/shadowsocks/go-shadowsocks2) ++ `dial-direct-proxy`: 当URL未命中RVPN规则,切换到直连时使用代理,常用于与其他代理工具配合的场景,目前仅支持http代理。 例如:`http://127.0.0.1:7890"`,为 `""` 时不启用 + + `tun-mode`: TUN 模式(实验性)。请阅读后文中的 TUN 模式注意事项 + `add-route`: 启用 TUN 模式时根据服务端下发配置添加路由 diff --git a/README_en.md b/README_en.md index 6c271b7..b27a86c 100644 --- a/README_en.md +++ b/README_en.md @@ -334,6 +334,8 @@ docker compose up -d + `shadowsocks-url`: Shadowsocks server URL. For example: `ss://aes-128-gcm:password@server:port`. Format [refer to here](https://github.com/shadowsocks/go-shadowsocks2) ++ `dial-direct-proxy`: When a URL does not match RVPN rules and switches to direct connection, it uses a proxy, typically in scenarios where it works in conjunction with other proxy tools. Currently, only HTTP proxies are supported. For example: `http://127.0.0.1:7890`, setting it to empty string (`""`) will disable its use. + + `tun-mode`: TUN mode (experimental). Please read the TUN mode precautions below + `add-route`: Add route according to the configuration issued by the server when TUN mode is enabled diff --git a/config.toml.example b/config.toml.example index b71ecf6..c5111ce 100644 --- a/config.toml.example +++ b/config.toml.example @@ -14,6 +14,7 @@ socks_user = "" socks_passwd = "" http_bind = ":1081" shadowsocks_url = "" # "ss://aes-128-gcm:password@:1082" +dial_direct_proxy = "" # "http://127.0.0.1:7890" or "socks://127.0.0.1:7890" tun_mode = false add_route = false dns_ttl = 3600 diff --git a/configs/config.go b/configs/config.go index 95e411b..1d760e1 100644 --- a/configs/config.go +++ b/configs/config.go @@ -16,6 +16,7 @@ type ( SocksPasswd string HTTPBind string ShadowsocksURL string + DialDirectProxy string TUNMode bool AddRoute bool DNSTTL uint64 @@ -59,6 +60,7 @@ type ( SocksPasswd *string `toml:"socks_passwd"` HTTPBind *string `toml:"http_bind"` ShadowsocksURL *string `toml:"shadowsocks_url"` + DialDirectProxy *string `toml:"dial_direct_proxy"` TUNMode *bool `toml:"tun_mode"` AddRoute *bool `toml:"add_route"` DNSTTL *uint64 `toml:"dns_ttl"` diff --git a/dial/dialer.go b/dial/dialer.go index b20ce01..ac145dd 100644 --- a/dial/dialer.go +++ b/dial/dialer.go @@ -16,45 +16,82 @@ import ( ) type Dialer struct { - stack stack.Stack - resolver *resolve.Resolver - ipResource *netaddr.IPSet - alwaysUseVPN bool + stack stack.Stack + resolver *resolve.Resolver + ipResource *netaddr.IPSet + alwaysUseVPN bool + dialDirectHTTPProxy string // format: "ip:port" + dialDirectSocksProxy string // WORKING IN PROCESS } -func dialDirect(ctx context.Context, network, addr string) (net.Conn, error) { - goDialer := &net.Dialer{} - goDial := goDialer.DialContext - - log.Printf("%s -> DIRECT", addr) +// dialDirectIP need have a `hostAddr` parameter, which will be passed to PROXY. But `hostAddr` maybe empty, ipAddr never be empty. +func (d *Dialer) dialDirectIP(ctx context.Context, network, ipAddr string, hostAddr string) (net.Conn, error) { + // only support http proxy now and tcp network type + if d.dialDirectHTTPProxy != "" && network == "tcp" { + usedAddr := ipAddr + if hostAddr != "" { + usedAddr = hostAddr + } + return d.dialDirectWithHTTPProxy(ctx, usedAddr) + // only support tcp for socks proxy + } else if d.dialDirectSocksProxy != "" && network == "tcp" { + if hostAddr != "" { + return d.dialDirectWithSocksProxy(ctx, network, hostAddr, false) + } else { + return d.dialDirectWithSocksProxy(ctx, network, ipAddr, true) + } + } else { + return d.dialDirectWithoutProxy(ctx, network, ipAddr) + } +} - return goDial(ctx, network, addr) +func (d *Dialer) dialDirectHost(ctx context.Context, network, hostAddr string) (net.Conn, error) { + // only support http proxy now and tcp network type + if d.dialDirectHTTPProxy != "" && network == "tcp" { + return d.dialDirectWithHTTPProxy(ctx, hostAddr) + // only support tcp for socks proxy + } else if d.dialDirectSocksProxy != "" && network == "tcp" { + return d.dialDirectWithSocksProxy(ctx, network, hostAddr, false) + } else { + return d.dialDirectWithoutProxy(ctx, network, hostAddr) + } } -func (d *Dialer) DialIPPort(ctx context.Context, network, addr string) (net.Conn, error) { +func (d *Dialer) DialIPPort(ctx context.Context, network, ipAddr string) (net.Conn, error) { + hostAddr := "" + if _, hostAddrOK := ctx.Value("RESOLVE_HOST").(string); hostAddrOK { + // hostAddr doesn't have port field at now + hostAddr = ctx.Value("RESOLVE_HOST").(string) + } + parts := strings.Split(ipAddr, ":") + if len(parts) >= 2 { + // maybe need extra check for parts[len(parts)-1] is port or not? + hostAddr += ":" + parts[len(parts)-1] + } + // If addr is IPv6, use direct connection - if strings.Count(addr, ":") > 1 { - return dialDirect(ctx, network, addr) + if len(parts) > 2 { + return d.dialDirectIP(ctx, network, ipAddr, hostAddr) } - host, portStr, err := net.SplitHostPort(addr) + ip, portStr, err := net.SplitHostPort(ipAddr) if err != nil { - return nil, errors.New("Invalid address: " + addr) + return nil, errors.New("Invalid address: " + ipAddr) } port, err := strconv.Atoi(portStr) if err != nil { - return nil, errors.New("Invalid port in address: " + addr) + return nil, errors.New("Invalid port in address: " + ipAddr) } var useVPN = false var target *net.IPAddr - if pureIp := net.ParseIP(host); pureIp != nil { + if pureIp := net.ParseIP(ip); pureIp != nil { target = &net.IPAddr{IP: pureIp} } else { - log.Printf("Illegal situation, host is not pure IP format: %s", host) - return dialDirect(ctx, network, addr) + log.Printf("Illegal situation, host is not pure IP format: %s", ip) + return d.dialDirectIP(ctx, network, ipAddr, hostAddr) } if d.alwaysUseVPN { @@ -76,59 +113,70 @@ func (d *Dialer) DialIPPort(ctx context.Context, network, addr string) (net.Conn if useVPN { if network == "tcp" { - log.Printf("%s -> VPN", addr) + log.Printf("%s -> VPN", ipAddr) return d.stack.DialTCP(&net.TCPAddr{ IP: target.IP, Port: port, }) } else if network == "udp" { - log.Printf("%s -> VPN", addr) + log.Printf("%s -> VPN", ipAddr) return d.stack.DialUDP(&net.UDPAddr{ IP: target.IP, Port: port, }) } else { - log.Printf("VPN only support TCP/UDP. Connection to %s will use direct connection", addr) - return dialDirect(ctx, network, addr) + log.Printf("VPN only support TCP/UDP. Connection to %s will use direct connection", ipAddr) + return d.dialDirectIP(ctx, network, ipAddr, hostAddr) } } else { - return dialDirect(ctx, network, addr) + return d.dialDirectIP(ctx, network, ipAddr, hostAddr) } } func (d *Dialer) Dial(ctx context.Context, network string, addr string) (net.Conn, error) { // If addr is IPv6, use direct connection if strings.Count(addr, ":") > 1 { - return dialDirect(ctx, network, addr) + return d.dialDirectIP(ctx, network, addr, "") } host, port, err := net.SplitHostPort(addr) if err != nil { - return dialDirect(ctx, network, addr) + return d.dialDirectHost(ctx, network, addr) } var ip net.IP if ip = net.ParseIP(host); ip == nil { ctx, ip, err = d.resolver.Resolve(ctx, host) if err != nil { - return dialDirect(ctx, network, addr) + return d.dialDirectHost(ctx, network, addr) } if strings.Count(ip.String(), ":") > 0 { - return dialDirect(ctx, network, addr) + return d.dialDirectIP(ctx, network, ip.String()+":"+port, addr) } } return d.DialIPPort(ctx, network, ip.String()+":"+port) } -func NewDialer(stack stack.Stack, resolver *resolve.Resolver, ipResource *netaddr.IPSet, alwaysUseVPN bool) *Dialer { +func NewDialer(stack stack.Stack, resolver *resolve.Resolver, ipResource *netaddr.IPSet, alwaysUseVPN bool, dialDirectProxy string) *Dialer { + dialHttpProxy := "" + dialSocksProxy := "" + if strings.HasPrefix(dialDirectProxy, "http://") { + dialHttpProxy = strings.TrimPrefix(dialDirectProxy, "http://") + } else if strings.HasPrefix(dialDirectProxy, "socks://") { + dialSocksProxy = strings.TrimPrefix(dialDirectProxy, "socks://") + } else if len(dialDirectProxy) > 0 { + log.Println("暂不支持除[http/socks]之外的DialDirectProxy,忽略该配置项") + } return &Dialer{ - stack: stack, - resolver: resolver, - ipResource: ipResource, - alwaysUseVPN: alwaysUseVPN, + stack: stack, + resolver: resolver, + ipResource: ipResource, + alwaysUseVPN: alwaysUseVPN, + dialDirectHTTPProxy: dialHttpProxy, + dialDirectSocksProxy: dialSocksProxy, } } diff --git a/dial/dialer_proxy.go b/dial/dialer_proxy.go new file mode 100644 index 0000000..4b19898 --- /dev/null +++ b/dial/dialer_proxy.go @@ -0,0 +1,129 @@ +package dial + +import ( + "context" + "errors" + "github.com/mythologyli/zju-connect/log" + "github.com/things-go/go-socks5/statute" + "net" + "strconv" + "strings" +) + +func (d *Dialer) dialDirectWithoutProxy(ctx context.Context, network, addr string) (net.Conn, error) { + goDialer := &net.Dialer{} + goDial := goDialer.DialContext + log.Printf("%s -> DIRECT", addr) + return goDial(ctx, network, addr) +} + +// usedAddr maybe ip:port or hostname:port, it doesn't matter +func (d *Dialer) dialDirectWithHTTPProxy(ctx context.Context, usedAddr string) (net.Conn, error) { + goDialer := &net.Dialer{} + goDial := goDialer.DialContext + + log.Printf("%s -> PROXY[%s]", usedAddr, d.dialDirectHTTPProxy) + conn, err := goDial(ctx, "tcp", d.dialDirectHTTPProxy) + if err != nil { + return nil, err + } + _, _ = conn.Write([]byte("CONNECT " + usedAddr + " HTTP/1.1\r\n\r\n")) + connBuf := make([]byte, 256) + totalNum := 0 + nowNum := 0 + for !strings.Contains(string(connBuf), "\r\n\r\n") { + nowNum, err = conn.Read(connBuf[totalNum:]) + totalNum += nowNum + if err != nil { + return nil, err + } + } + if strings.Contains(string(connBuf[:totalNum]), "200") { + return conn, nil + } else { + return nil, errors.New("PROXY CONNECT ERROR") + } +} + +func (d *Dialer) dialDirectWithSocksProxy(ctx context.Context, network, usedAddr string, isIP bool) (net.Conn, error) { + goDialer := &net.Dialer{} + goDial := goDialer.DialContext + + log.Printf("%s -> PROXY[%s]", usedAddr, d.dialDirectSocksProxy) + conn, err := goDial(ctx, "tcp", d.dialDirectSocksProxy) + if err != nil { + return nil, err + } + _, err = conn.Write(statute.NewMethodRequest(statute.VersionSocks5, []byte{statute.MethodNoAuth}).Bytes()) + if err != nil { + return nil, err + } + methodReply, err := statute.ParseMethodReply(conn) + if err != nil || methodReply.Method != statute.MethodNoAuth || methodReply.Ver != statute.VersionSocks5 { + return nil, errors.New("SOCKS5 METHOD ERROR") + } + + parts := strings.Split(usedAddr, ":") + dstAddr := statute.AddrSpec{} + if isIP { + if len(parts) > 2 { + dstAddr.AddrType = statute.ATYPIPv6 + dstAddr.IP = net.ParseIP(strings.TrimSuffix(usedAddr, ":"+parts[len(parts)-1])) + if dstAddr.IP == nil { + return nil, errors.New("Invalid address for socks proxy: " + usedAddr) + } + dstAddr.Port, err = strconv.Atoi(parts[len(parts)-1]) + if err != nil { + return nil, errors.New("Invalid port for socks proxy: " + usedAddr) + } + } else if len(parts) == 2 { + dstAddr.AddrType = statute.ATYPIPv4 + dstAddr.IP = net.ParseIP(parts[0]) + if dstAddr.IP == nil { + return nil, errors.New("Invalid address for socks proxy: " + usedAddr) + } + dstAddr.Port, err = strconv.Atoi(parts[1]) + if err != nil { + return nil, errors.New("Invalid port for socks proxy: " + usedAddr) + } + } else { + return nil, errors.New("Invalid address for socks proxy: " + usedAddr) + } + } else { + if len(parts) == 2 { + dstAddr.AddrType = statute.ATYPDomain + dstAddr.FQDN = parts[0] + dstAddr.Port, err = strconv.Atoi(parts[1]) + if err != nil { + return nil, errors.New("Invalid port for socks proxy: " + usedAddr) + } + } else { + return nil, errors.New("Invalid address for socks proxy: " + usedAddr) + } + } + var command byte + if network == "tcp" { + command = statute.CommandConnect + } else { + // not support yet! + command = statute.CommandAssociate + } + req := statute.Request{ + Version: statute.VersionSocks5, + Command: command, + Reserved: 0, + DstAddr: dstAddr, + } + _, err = conn.Write(req.Bytes()) + if err != nil { + return nil, err + } + reply, err := statute.ParseReply(conn) + if err != nil { + return nil, err + } + if reply.Version != statute.VersionSocks5 || reply.Response != statute.RepSuccess { + return nil, errors.New("SOCKS5 CONNECT ERROR") + } + return conn, nil +} diff --git a/init.go b/init.go index 2ef3d78..6c14c8c 100644 --- a/init.go +++ b/init.go @@ -41,6 +41,7 @@ func parseTOMLConfig(configFile string, conf *configs.Config) error { conf.SocksPasswd = getTOMLVal(confTOML.SocksPasswd, "") conf.HTTPBind = getTOMLVal(confTOML.HTTPBind, ":1081") conf.ShadowsocksURL = getTOMLVal(confTOML.ShadowsocksURL, "") + conf.DialDirectProxy = getTOMLVal(confTOML.DialDirectProxy, "") conf.TUNMode = getTOMLVal(confTOML.TUNMode, false) conf.AddRoute = getTOMLVal(confTOML.AddRoute, false) conf.DNSTTL = getTOMLVal(confTOML.DNSTTL, uint64(3600)) @@ -117,6 +118,7 @@ func init() { flag.StringVar(&conf.SocksPasswd, "socks-passwd", "", "SOCKS5 password, default is don't use auth") flag.StringVar(&conf.HTTPBind, "http-bind", ":1081", "The address HTTP server listens on (e.g. 127.0.0.1:1081)") flag.StringVar(&conf.ShadowsocksURL, "shadowsocks-url", "", "The address Shadowsocks server listens on (e.g. ss://method:password@host:port)") + flag.StringVar(&conf.DialDirectProxy, "dial-direct-proxy", "", "Dial with proxy when the connection doesn't match RVPN rules (e.g. http://127.0.0.1:7890)") flag.BoolVar(&conf.TUNMode, "tun-mode", false, "Enable TUN mode (experimental)") flag.BoolVar(&conf.AddRoute, "add-route", false, "Add route from rules for TUN interface") flag.Uint64Var(&conf.DNSTTL, "dns-ttl", 3600, "DNS record time to live, unit is second") diff --git a/main.go b/main.go index 793e431..4f62717 100644 --- a/main.go +++ b/main.go @@ -135,7 +135,7 @@ func main() { go vpnStack.Run() - vpnDialer := dial.NewDialer(vpnStack, vpnResolver, ipResource, conf.ProxyAll) + vpnDialer := dial.NewDialer(vpnStack, vpnResolver, ipResource, conf.ProxyAll, conf.DialDirectProxy) if conf.DNSServerBind != "" { go service.ServeDNS(conf.DNSServerBind, localResolver) diff --git a/main_tun.go b/main_tun.go index 0e1a78b..2591e2e 100644 --- a/main_tun.go +++ b/main_tun.go @@ -123,7 +123,7 @@ func main() { go vpnStack.Run() - vpnDialer := dial.NewDialer(vpnStack, vpnResolver, ipResource, conf.ProxyAll) + vpnDialer := dial.NewDialer(vpnStack, vpnResolver, ipResource, conf.ProxyAll, conf.DialDirectProxy) if conf.DNSServerBind != "" { go service.ServeDNS(conf.DNSServerBind, localResolver) diff --git a/resolve/resolver.go b/resolve/resolver.go index 39e5f7f..3a21339 100644 --- a/resolve/resolver.go +++ b/resolve/resolver.go @@ -33,8 +33,13 @@ type Resolver struct { concurResolveLock sync.Map } -// Resolve ip address. If the host should be visited via VPN, this function set a USE_VPN value in context -func (r *Resolver) Resolve(ctx context.Context, host string) (context.Context, net.IP, error) { +// Resolve ip address. If the host should be visited via VPN, this function set a USE_VPN value in context. If resolve success, this function set a RESOLVE_HOST value in context. +func (r *Resolver) Resolve(ctx context.Context, host string) (resCtx context.Context, resIP net.IP, resErr error) { + defer func() { + if resErr == nil { + resCtx = context.WithValue(resCtx, "RESOLVE_HOST", host) + } + }() var useVPN = false if r.domainResource != nil { if r.domainResource.FindMatchDomainSuffixPayload(host) {