├── pkg ├── conns │ ├── faketcp │ │ ├── LICENSE │ │ ├── tcp_stub.go │ │ ├── obfs.go │ │ └── tcp_test.go │ ├── udp │ │ └── obfs.go │ └── wechat │ │ └── obfs.go ├── obfs │ ├── obfs.go │ ├── dummy.go │ ├── xplus_test.go │ └── xplus.go ├── pmtud_fix │ ├── avail.go │ └── unavail.go ├── sockopt │ ├── sockopt_others.go │ ├── sockopt_linux.go │ └── sockopt.go ├── tproxy │ ├── tcp_stub.go │ ├── udp_stub.go │ ├── tcp_linux.go │ └── udp_linux.go ├── redirect │ ├── tcp_stub.go │ ├── origdst_linux.go │ ├── origdst_linux_386.go │ └── tcp_linux.go ├── utils │ ├── misc.go │ └── pipe.go ├── tun │ ├── tcp.go │ ├── udp.go │ └── server.go ├── core │ ├── stream.go │ ├── frag.go │ ├── protocol.go │ ├── server.go │ ├── frag_test.go │ ├── client.go │ └── server_client.go ├── relay │ ├── tcp.go │ └── udp.go ├── transport │ ├── resolve.go │ ├── client.go │ ├── server.go │ └── socks5.go ├── acl │ ├── entry_test.go │ ├── engine_test.go │ ├── engine.go │ └── entry.go ├── congestion │ ├── pacer.go │ └── brutal.go └── http │ └── server.go ├── docs ├── bench │ └── bench.png ├── logos │ ├── readme.png │ ├── whitebg_black.png │ ├── whitebg_pink.png │ ├── transparent_pink.png │ └── transparent_black.png └── socks5 │ ├── udpchk.py │ └── tcping.py ├── docker-compose.yaml ├── cmd ├── acme.go ├── auth │ ├── cmd.go │ └── http.go ├── ipmasker.go ├── config_test.go ├── update.go ├── mmdb.go ├── client_nongpl.go ├── completion.go ├── kploader.go ├── resolver.go ├── client_gpl.go ├── main.go ├── config.go └── server.go ├── .github └── workflows │ ├── build-master.yml │ └── release.yml ├── LICENSE.md ├── README.md ├── Dockerfile ├── CHANGELOG.md ├── Taskfile.yaml ├── .gitignore └── go.mod /pkg/conns/faketcp/LICENSE: -------------------------------------------------------------------------------- 1 | Grabbed from https://github.com/xtaci/tcpraw with modifications -------------------------------------------------------------------------------- /docs/bench/bench.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatsuriDayo/hysteria/HEAD/docs/bench/bench.png -------------------------------------------------------------------------------- /docs/logos/readme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatsuriDayo/hysteria/HEAD/docs/logos/readme.png -------------------------------------------------------------------------------- /docs/logos/whitebg_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatsuriDayo/hysteria/HEAD/docs/logos/whitebg_black.png -------------------------------------------------------------------------------- /docs/logos/whitebg_pink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatsuriDayo/hysteria/HEAD/docs/logos/whitebg_pink.png -------------------------------------------------------------------------------- /docs/logos/transparent_pink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatsuriDayo/hysteria/HEAD/docs/logos/transparent_pink.png -------------------------------------------------------------------------------- /docs/logos/transparent_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatsuriDayo/hysteria/HEAD/docs/logos/transparent_black.png -------------------------------------------------------------------------------- /pkg/obfs/obfs.go: -------------------------------------------------------------------------------- 1 | package obfs 2 | 3 | type Obfuscator interface { 4 | Deobfuscate(in []byte, out []byte) int 5 | Obfuscate(in []byte, out []byte) int 6 | } 7 | -------------------------------------------------------------------------------- /pkg/pmtud_fix/avail.go: -------------------------------------------------------------------------------- 1 | //go:build linux || windows 2 | // +build linux windows 3 | 4 | package pmtud_fix 5 | 6 | const ( 7 | DisablePathMTUDiscovery = false 8 | ) 9 | -------------------------------------------------------------------------------- /pkg/pmtud_fix/unavail.go: -------------------------------------------------------------------------------- 1 | //go:build !linux && !windows 2 | // +build !linux,!windows 3 | 4 | package pmtud_fix 5 | 6 | const ( 7 | DisablePathMTUDiscovery = true 8 | ) 9 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | services: 3 | hysteria: 4 | image: tobyxdd/hysteria 5 | container_name: hysteria 6 | restart: always 7 | network_mode: "host" 8 | volumes: 9 | - ./hysteria.json:/etc/hysteria.json 10 | command: ["server", "--config", "/etc/hysteria.json"] 11 | -------------------------------------------------------------------------------- /pkg/sockopt/sockopt_others.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | 3 | package sockopt 4 | 5 | import ( 6 | "errors" 7 | "net" 8 | "syscall" 9 | ) 10 | 11 | func bindRawConn(network string, c syscall.RawConn, bindIface *net.Interface) error { 12 | return errors.New("binding interface is not supported on the current system") 13 | } 14 | -------------------------------------------------------------------------------- /pkg/obfs/dummy.go: -------------------------------------------------------------------------------- 1 | package obfs 2 | 3 | type DummyObfuscator struct{} 4 | 5 | func NewDummyObfuscator() *DummyObfuscator { 6 | return &DummyObfuscator{} 7 | } 8 | 9 | func (x *DummyObfuscator) Deobfuscate(in []byte, out []byte) int { 10 | if len(out) < len(in) { 11 | return 0 12 | } 13 | return copy(out, in) 14 | } 15 | 16 | func (x *DummyObfuscator) Obfuscate(in []byte, out []byte) int { 17 | return copy(out, in) 18 | } 19 | -------------------------------------------------------------------------------- /pkg/sockopt/sockopt_linux.go: -------------------------------------------------------------------------------- 1 | package sockopt 2 | 3 | import ( 4 | "net" 5 | "syscall" 6 | 7 | "golang.org/x/sys/unix" 8 | ) 9 | 10 | func bindRawConn(network string, c syscall.RawConn, bindIface *net.Interface) error { 11 | var err1, err2 error 12 | err1 = c.Control(func(fd uintptr) { 13 | if bindIface != nil { 14 | err2 = unix.BindToDevice(int(fd), bindIface.Name) 15 | } 16 | }) 17 | if err1 != nil { 18 | return err1 19 | } else { 20 | return err2 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /pkg/conns/faketcp/tcp_stub.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | // +build !linux 3 | 4 | package faketcp 5 | 6 | import ( 7 | "errors" 8 | "net" 9 | ) 10 | 11 | type TCPConn struct{ *net.UDPConn } 12 | 13 | // Dial connects to the remote TCP port, 14 | // and returns a single packet-oriented connection 15 | func Dial(network, address string) (*TCPConn, error) { 16 | return nil, errors.New("faketcp is not supported on this platform") 17 | } 18 | 19 | func Listen(network, address string) (*TCPConn, error) { 20 | return nil, errors.New("faketcp is not supported on this platform") 21 | } 22 | -------------------------------------------------------------------------------- /pkg/tproxy/tcp_stub.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | // +build !linux 3 | 4 | package tproxy 5 | 6 | import ( 7 | "errors" 8 | "net" 9 | "time" 10 | 11 | "github.com/HyNetwork/hysteria/pkg/core" 12 | ) 13 | 14 | type TCPTProxy struct{} 15 | 16 | func NewTCPTProxy(hyClient *core.Client, listen string, timeout time.Duration, 17 | connFunc func(addr, reqAddr net.Addr), 18 | errorFunc func(addr, reqAddr net.Addr, err error), 19 | ) (*TCPTProxy, error) { 20 | return nil, errors.New("not supported on the current system") 21 | } 22 | 23 | func (r *TCPTProxy) ListenAndServe() error { 24 | return nil 25 | } 26 | -------------------------------------------------------------------------------- /pkg/redirect/tcp_stub.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | // +build !linux 3 | 4 | package redirect 5 | 6 | import ( 7 | "errors" 8 | "net" 9 | "time" 10 | 11 | "github.com/HyNetwork/hysteria/pkg/core" 12 | ) 13 | 14 | type TCPRedirect struct{} 15 | 16 | func NewTCPRedirect(hyClient *core.Client, listen string, timeout time.Duration, 17 | connFunc func(addr, reqAddr net.Addr), 18 | errorFunc func(addr, reqAddr net.Addr, err error), 19 | ) (*TCPRedirect, error) { 20 | return nil, errors.New("not supported on the current system") 21 | } 22 | 23 | func (r *TCPRedirect) ListenAndServe() error { 24 | return nil 25 | } 26 | -------------------------------------------------------------------------------- /docs/socks5/udpchk.py: -------------------------------------------------------------------------------- 1 | import socks 2 | import socket 3 | 4 | 5 | def main(): 6 | s = socks.socksocket(socket.AF_INET, socket.SOCK_DGRAM) 7 | s.set_proxy(socks.SOCKS5, "127.0.0.1", 1080) 8 | # Raw DNS request 9 | req = b"\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x05\x62\x61\x69\x64\x75\x03\x63\x6f\x6d\x00\x00\x01\x00\x01" 10 | s.sendto(req, ("8.8.8.8", 53)) 11 | (rsp, address) = s.recvfrom(4096) 12 | if rsp[0] == req[0] and rsp[1] == req[1]: 13 | print("UDP check passed") 14 | else: 15 | print("Invalid response") 16 | 17 | 18 | if __name__ == "__main__": 19 | main() 20 | -------------------------------------------------------------------------------- /pkg/sockopt/sockopt.go: -------------------------------------------------------------------------------- 1 | package sockopt 2 | 3 | import ( 4 | "net" 5 | "syscall" 6 | ) 7 | 8 | // https://github.com/v2fly/v2ray-core/blob/4e247840821f3dd326722d4db02ee3c237074fc2/transport/internet/config.pb.go#L420-L426 9 | 10 | func BindDialer(d *net.Dialer, intf *net.Interface) { 11 | d.Control = func(network, address string, c syscall.RawConn) error { 12 | return bindRawConn(network, c, intf) 13 | } 14 | } 15 | 16 | func BindUDPConn(network string, conn *net.UDPConn, intf *net.Interface) error { 17 | c, err := conn.SyscallConn() 18 | if err != nil { 19 | return err 20 | } 21 | 22 | return bindRawConn(network, c, intf) 23 | } 24 | -------------------------------------------------------------------------------- /pkg/tproxy/udp_stub.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | // +build !linux 3 | 4 | package tproxy 5 | 6 | import ( 7 | "errors" 8 | "net" 9 | "time" 10 | 11 | "github.com/HyNetwork/hysteria/pkg/core" 12 | ) 13 | 14 | var ErrTimeout = errors.New("inactivity timeout") 15 | 16 | type UDPTProxy struct{} 17 | 18 | func NewUDPTProxy(hyClient *core.Client, listen string, timeout time.Duration, 19 | connFunc func(addr, reqAddr net.Addr), errorFunc func(addr, reqAddr net.Addr, err error), 20 | ) (*UDPTProxy, error) { 21 | return nil, errors.New("not supported on the current system") 22 | } 23 | 24 | func (r *UDPTProxy) ListenAndServe() error { 25 | return nil 26 | } 27 | -------------------------------------------------------------------------------- /cmd/acme.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | 7 | "github.com/caddyserver/certmagic" 8 | ) 9 | 10 | func acmeTLSConfig(domains []string, email string, disableHTTP bool, disableTLSALPN bool, 11 | altHTTPPort int, altTLSALPNPort int, 12 | ) (*tls.Config, error) { 13 | certmagic.DefaultACME.Agreed = true 14 | certmagic.DefaultACME.Email = email 15 | certmagic.DefaultACME.DisableHTTPChallenge = disableHTTP 16 | certmagic.DefaultACME.DisableTLSALPNChallenge = disableTLSALPN 17 | certmagic.DefaultACME.AltHTTPPort = altHTTPPort 18 | certmagic.DefaultACME.AltTLSALPNPort = altTLSALPNPort 19 | cfg := certmagic.NewDefault() 20 | return cfg.TLSConfig(), cfg.ManageSync(context.Background(), domains) 21 | } 22 | -------------------------------------------------------------------------------- /cmd/auth/cmd.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "net" 5 | "os/exec" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | type CmdAuthProvider struct { 13 | Cmd string 14 | } 15 | 16 | func (p *CmdAuthProvider) Auth(addr net.Addr, auth []byte, sSend uint64, sRecv uint64) (bool, string) { 17 | cmd := exec.Command(p.Cmd, addr.String(), string(auth), strconv.Itoa(int(sSend)), strconv.Itoa(int(sRecv))) 18 | out, err := cmd.Output() 19 | if err != nil { 20 | if _, ok := err.(*exec.ExitError); ok { 21 | return false, strings.TrimSpace(string(out)) 22 | } else { 23 | logrus.WithFields(logrus.Fields{ 24 | "error": err, 25 | }).Error("Failed to execute auth command") 26 | return false, "internal error" 27 | } 28 | } else { 29 | return true, strings.TrimSpace(string(out)) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /docs/socks5/tcping.py: -------------------------------------------------------------------------------- 1 | import socks 2 | import socket 3 | import time 4 | 5 | target = "1.1.1.1" 6 | 7 | 8 | def main(): 9 | s = socks.socksocket(socket.AF_INET, socket.SOCK_STREAM) 10 | s.set_proxy(socks.SOCKS5, "127.0.0.1", 1080) 11 | 12 | print("Sending HTTP request to %s" % target) 13 | start = time.time() 14 | s.connect((target, 80)) 15 | s.send(b"GET / HTTP/1.1\r\nHost: " + target.encode() + b"\r\n\r\n") 16 | data = s.recv(1024) 17 | if not data: 18 | print("No data received") 19 | elif not data.startswith(b"HTTP/1.1 "): 20 | print("Invalid response received") 21 | else: 22 | print("Response received") 23 | end = time.time() 24 | s.close() 25 | 26 | print("Time: {} ms".format(round((end - start) * 1000, 2))) 27 | 28 | 29 | if __name__ == "__main__": 30 | main() 31 | -------------------------------------------------------------------------------- /pkg/utils/misc.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "net" 5 | "strconv" 6 | ) 7 | 8 | func SplitHostPort(hostport string) (string, uint16, error) { 9 | host, port, err := net.SplitHostPort(hostport) 10 | if err != nil { 11 | return "", 0, err 12 | } 13 | portUint, err := strconv.ParseUint(port, 10, 16) 14 | if err != nil { 15 | return "", 0, err 16 | } 17 | return host, uint16(portUint), err 18 | } 19 | 20 | func ParseIPZone(s string) (net.IP, string) { 21 | s, zone := splitHostZone(s) 22 | return net.ParseIP(s), zone 23 | } 24 | 25 | func splitHostZone(s string) (host, zone string) { 26 | if i := last(s, '%'); i > 0 { 27 | host, zone = s[:i], s[i+1:] 28 | } else { 29 | host = s 30 | } 31 | return 32 | } 33 | 34 | func last(s string, b byte) int { 35 | i := len(s) 36 | for i--; i >= 0; i-- { 37 | if s[i] == b { 38 | break 39 | } 40 | } 41 | return i 42 | } 43 | -------------------------------------------------------------------------------- /cmd/ipmasker.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net" 5 | ) 6 | 7 | type ipMasker struct { 8 | IPv4Mask net.IPMask 9 | IPv6Mask net.IPMask 10 | } 11 | 12 | // Mask masks an address with the configured CIDR. 13 | // addr can be "host:port" or just host. 14 | func (m *ipMasker) Mask(addr string) string { 15 | if m.IPv4Mask == nil && m.IPv6Mask == nil { 16 | return addr 17 | } 18 | 19 | host, port, err := net.SplitHostPort(addr) 20 | if err != nil { 21 | // just host 22 | host, port = addr, "" 23 | } 24 | ip := net.ParseIP(host) 25 | if ip == nil { 26 | // not an IP address, return as is 27 | return addr 28 | } 29 | if ip4 := ip.To4(); ip4 != nil && m.IPv4Mask != nil { 30 | // IPv4 31 | host = ip4.Mask(m.IPv4Mask).String() 32 | } else if ip6 := ip.To16(); ip6 != nil && m.IPv6Mask != nil { 33 | // IPv6 34 | host = ip6.Mask(m.IPv6Mask).String() 35 | } 36 | if port != "" { 37 | return net.JoinHostPort(host, port) 38 | } else { 39 | return host 40 | } 41 | } 42 | 43 | var defaultIPMasker = &ipMasker{} 44 | -------------------------------------------------------------------------------- /pkg/redirect/origdst_linux.go: -------------------------------------------------------------------------------- 1 | //go:build !386 2 | // +build !386 3 | 4 | package redirect 5 | 6 | import ( 7 | "syscall" 8 | "unsafe" 9 | ) 10 | 11 | const ( 12 | SO_ORIGINAL_DST = 80 13 | IP6T_SO_ORIGINAL_DST = 80 14 | ) 15 | 16 | type sockAddr struct { 17 | family uint16 18 | port [2]byte // big endian regardless of host byte order 19 | data [24]byte // check sockaddr_in or sockaddr_in6 for more information 20 | } 21 | 22 | func getOrigDst(fd uintptr) (*sockAddr, error) { 23 | var addr sockAddr 24 | addrSize := uint32(unsafe.Sizeof(addr)) 25 | // try IPv6 first 26 | _, _, err := syscall.Syscall6(syscall.SYS_GETSOCKOPT, fd, syscall.SOL_IPV6, IP6T_SO_ORIGINAL_DST, 27 | uintptr(unsafe.Pointer(&addr)), uintptr(unsafe.Pointer(&addrSize)), 0) 28 | if err != 0 { 29 | // try IPv4 30 | _, _, err = syscall.Syscall6(syscall.SYS_GETSOCKOPT, fd, syscall.SOL_IP, SO_ORIGINAL_DST, 31 | uintptr(unsafe.Pointer(&addr)), uintptr(unsafe.Pointer(&addrSize)), 0) 32 | if err != 0 { 33 | // failed 34 | return nil, err 35 | } 36 | } 37 | return &addr, nil 38 | } 39 | -------------------------------------------------------------------------------- /pkg/redirect/origdst_linux_386.go: -------------------------------------------------------------------------------- 1 | package redirect 2 | 3 | import ( 4 | "syscall" 5 | "unsafe" 6 | ) 7 | 8 | const ( 9 | SYS_GETSOCKOPT = 15 10 | SO_ORIGINAL_DST = 80 11 | IP6T_SO_ORIGINAL_DST = 80 12 | ) 13 | 14 | type sockAddr struct { 15 | family uint16 16 | port [2]byte // big endian regardless of host byte order 17 | data [24]byte // check sockaddr_in or sockaddr_in6 for more information 18 | } 19 | 20 | func getOrigDst(fd uintptr) (*sockAddr, error) { 21 | var addr sockAddr 22 | addrSize := uint32(unsafe.Sizeof(addr)) 23 | // try IPv6 first 24 | _, _, err := syscall.Syscall6(syscall.SYS_SOCKETCALL, SYS_GETSOCKOPT, fd, syscall.SOL_IPV6, IP6T_SO_ORIGINAL_DST, 25 | uintptr(unsafe.Pointer(&addr)), uintptr(unsafe.Pointer(&addrSize))) 26 | if err != 0 { 27 | // try IPv4 28 | _, _, err = syscall.Syscall6(syscall.SYS_SOCKETCALL, SYS_GETSOCKOPT, fd, syscall.SOL_IP, SO_ORIGINAL_DST, 29 | uintptr(unsafe.Pointer(&addr)), uintptr(unsafe.Pointer(&addrSize))) 30 | if err != 0 { 31 | // failed 32 | return nil, err 33 | } 34 | } 35 | return &addr, nil 36 | } 37 | -------------------------------------------------------------------------------- /pkg/obfs/xplus_test.go: -------------------------------------------------------------------------------- 1 | package obfs 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestXPlusObfuscator(t *testing.T) { 9 | x := NewXPlusObfuscator([]byte("Vaundy")) 10 | tests := []struct { 11 | name string 12 | p []byte 13 | }{ 14 | {name: "1", p: []byte("HelloWorld")}, 15 | {name: "2", p: []byte("Regret is just a horrible attempt at time travel that ends with you feeling like crap")}, 16 | {name: "3", p: []byte("To be, or not to be, that is the question:\nWhether 'tis nobler in the mind to suffer\n" + 17 | "The slings and arrows of outrageous fortune,\nOr to take arms against a sea of troubles\n" + 18 | "And by opposing end them. To die—to sleep,\nNo more; and by a sleep to say we end")}, 19 | {name: "empty", p: []byte("")}, 20 | } 21 | for _, tt := range tests { 22 | t.Run(tt.name, func(t *testing.T) { 23 | buf := make([]byte, 10240) 24 | n := x.Obfuscate(tt.p, buf) 25 | n2 := x.Deobfuscate(buf[:n], buf[n:]) 26 | if !bytes.Equal(tt.p, buf[n:n+n2]) { 27 | t.Errorf("Inconsistent deobfuscate result: got %v, want %v", buf[n:n+n2], tt.p) 28 | } 29 | }) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pkg/tun/tcp.go: -------------------------------------------------------------------------------- 1 | //go:build gpl 2 | // +build gpl 3 | 4 | package tun 5 | 6 | import ( 7 | "net" 8 | 9 | "github.com/HyNetwork/hysteria/pkg/utils" 10 | "github.com/xjasonlyu/tun2socks/v2/core/adapter" 11 | ) 12 | 13 | func (s *Server) HandleTCP(localConn adapter.TCPConn) { 14 | go s.handleTCPConn(localConn) 15 | } 16 | 17 | func (s *Server) handleTCPConn(localConn adapter.TCPConn) { 18 | defer localConn.Close() 19 | 20 | id := localConn.ID() 21 | remoteAddr := net.TCPAddr{ 22 | IP: net.IP(id.LocalAddress), 23 | Port: int(id.LocalPort), 24 | } 25 | localAddr := net.TCPAddr{ 26 | IP: net.IP(id.RemoteAddress), 27 | Port: int(id.RemotePort), 28 | } 29 | 30 | if s.RequestFunc != nil { 31 | s.RequestFunc(&localAddr, remoteAddr.String()) 32 | } 33 | 34 | var err error 35 | defer func() { 36 | if s.ErrorFunc != nil && err != nil { 37 | s.ErrorFunc(&localAddr, remoteAddr.String(), err) 38 | } 39 | }() 40 | 41 | rc, err := s.HyClient.DialTCP(remoteAddr.String()) 42 | if err != nil { 43 | return 44 | } 45 | defer rc.Close() 46 | 47 | err = utils.PipePairWithTimeout(localConn, rc, s.Timeout) 48 | } 49 | -------------------------------------------------------------------------------- /.github/workflows/build-master.yml: -------------------------------------------------------------------------------- 1 | name: Build master 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | tags-ignore: 8 | - 'v*' 9 | 10 | jobs: 11 | 12 | build: 13 | name: Build 14 | runs-on: ubuntu-latest 15 | env: 16 | ACTIONS_ALLOW_UNSECURE_COMMANDS: true 17 | 18 | steps: 19 | 20 | - name: Check out 21 | uses: actions/checkout@v3 22 | 23 | - name: Get time 24 | uses: gerred/actions/current-time@master 25 | id: current-time 26 | 27 | - name: Build 28 | uses: tobyxdd/go-cross-build@d00fc41eb205f57dd90f6e5af4613e21c7ebe73f 29 | env: 30 | TIME: "${{ steps.current-time.outputs.time }}" 31 | CGO_ENABLED: "0" 32 | with: 33 | name: hysteria 34 | dest: dist 35 | ldflags: -w -s -X main.appCommit=${{ github.sha }} -X main.appDate=${{ env.TIME }} 36 | platforms: 'linux/amd64, linux/386, linux/arm, linux/arm64' 37 | package: cmd 38 | compress: false 39 | 40 | - name: Archive 41 | uses: actions/upload-artifact@v3 42 | with: 43 | name: dist 44 | path: dist 45 | -------------------------------------------------------------------------------- /cmd/config_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func Test_stringToBps(t *testing.T) { 6 | tests := []struct { 7 | name string 8 | s string 9 | want uint64 10 | }{ 11 | {name: "bps 1", s: "8 bps", want: 1}, 12 | {name: "bps 2", s: "3 bps", want: 0}, 13 | {name: "Bps", s: "9991Bps", want: 9991}, 14 | {name: "KBps", s: "10 KBps", want: 10240}, 15 | {name: "Kbps", s: "10 Kbps", want: 1280}, 16 | {name: "MBps", s: "10 MBps", want: 10485760}, 17 | {name: "Mbps", s: "10 Mbps", want: 1310720}, 18 | {name: "GBps", s: "10 GBps", want: 10737418240}, 19 | {name: "Gbps", s: "10 Gbps", want: 1342177280}, 20 | {name: "TBps", s: "10 TBps", want: 10995116277760}, 21 | {name: "Tbps", s: "10 Tbps", want: 1374389534720}, 22 | {name: "invalid 1", s: "6699E Kbps", want: 0}, 23 | {name: "invalid 2", s: "400 Bsp", want: 0}, 24 | {name: "invalid 3", s: "9 GBbps", want: 0}, 25 | {name: "invalid 4", s: "Mbps", want: 0}, 26 | } 27 | for _, tt := range tests { 28 | t.Run(tt.name, func(t *testing.T) { 29 | if got := stringToBps(tt.s); got != tt.want { 30 | t.Errorf("stringToBps() = %v, want %v", got, tt.want) 31 | } 32 | }) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /cmd/update.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | const githubAPIURL = "https://api.github.com/repos/HyNetwork/hysteria/releases/latest" 13 | 14 | type releaseInfo struct { 15 | URL string `json:"html_url"` 16 | TagName string `json:"tag_name"` 17 | CreatedAt string `json:"created_at"` 18 | PublishedAt string `json:"published_at"` 19 | } 20 | 21 | func checkUpdate() { 22 | info, err := fetchLatestRelease() 23 | if err == nil && info.TagName != appVersion { 24 | logrus.WithFields(logrus.Fields{ 25 | "version": info.TagName, 26 | "url": info.URL, 27 | }).Info("New version available") 28 | } 29 | } 30 | 31 | func fetchLatestRelease() (*releaseInfo, error) { 32 | hc := &http.Client{ 33 | Timeout: time.Second * 20, 34 | } 35 | resp, err := hc.Get(githubAPIURL) 36 | if err != nil { 37 | return nil, err 38 | } 39 | defer resp.Body.Close() 40 | body, err := ioutil.ReadAll(resp.Body) 41 | if err != nil { 42 | return nil, err 43 | } 44 | var info releaseInfo 45 | err = json.Unmarshal(body, &info) 46 | return &info, err 47 | } 48 | -------------------------------------------------------------------------------- /cmd/mmdb.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "os" 7 | 8 | "github.com/oschwald/geoip2-golang" 9 | "github.com/sirupsen/logrus" 10 | "github.com/spf13/viper" 11 | ) 12 | 13 | func downloadMMDB(filename string) error { 14 | resp, err := http.Get(viper.GetString("mmdb-url")) 15 | if err != nil { 16 | return err 17 | } 18 | defer resp.Body.Close() 19 | 20 | file, err := os.Create(filename) 21 | if err != nil { 22 | return err 23 | } 24 | defer file.Close() 25 | 26 | _, err = io.Copy(file, resp.Body) 27 | return err 28 | } 29 | 30 | func loadMMDBReader(filename string) (*geoip2.Reader, error) { 31 | if _, err := os.Stat(filename); err != nil { 32 | if os.IsNotExist(err) { 33 | logrus.Info("GeoLite2 database not found, downloading...") 34 | if err := downloadMMDB(filename); err != nil { 35 | return nil, err 36 | } 37 | logrus.WithFields(logrus.Fields{ 38 | "file": filename, 39 | }).Info("GeoLite2 database downloaded") 40 | return geoip2.Open(filename) 41 | } else { 42 | // some other error 43 | return nil, err 44 | } 45 | } else { 46 | // file exists, just open it 47 | return geoip2.Open(filename) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /pkg/obfs/xplus.go: -------------------------------------------------------------------------------- 1 | package obfs 2 | 3 | import ( 4 | "crypto/sha256" 5 | "math/rand" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | // [salt][obfuscated payload] 11 | 12 | const saltLen = 16 13 | 14 | type XPlusObfuscator struct { 15 | Key []byte 16 | RandSrc *rand.Rand 17 | 18 | lk sync.Mutex 19 | } 20 | 21 | func NewXPlusObfuscator(key []byte) *XPlusObfuscator { 22 | return &XPlusObfuscator{ 23 | Key: key, 24 | RandSrc: rand.New(rand.NewSource(time.Now().UnixNano())), 25 | } 26 | } 27 | 28 | func (x *XPlusObfuscator) Deobfuscate(in []byte, out []byte) int { 29 | pLen := len(in) - saltLen 30 | if pLen <= 0 || len(out) < pLen { 31 | // Invalid 32 | return 0 33 | } 34 | key := sha256.Sum256(append(x.Key, in[:saltLen]...)) 35 | // Deobfuscate the payload 36 | for i, c := range in[saltLen:] { 37 | out[i] = c ^ key[i%sha256.Size] 38 | } 39 | return pLen 40 | } 41 | 42 | func (x *XPlusObfuscator) Obfuscate(in []byte, out []byte) int { 43 | x.lk.Lock() 44 | _, _ = x.RandSrc.Read(out[:saltLen]) // salt 45 | x.lk.Unlock() 46 | // Obfuscate the payload 47 | key := sha256.Sum256(append(x.Key, out[:saltLen]...)) 48 | for i, c := range in { 49 | out[i+saltLen] = c ^ key[i%sha256.Size] 50 | } 51 | return len(in) + saltLen 52 | } 53 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | License 2 | ================== 3 | 4 | Hysteria itself, including all codes under this directory, is licensed under the MIT License. 5 | 6 | ``` 7 | The MIT License (MIT) 8 | 9 | Copyright (c) 2021 Toby 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining a copy 12 | of this software and associated documentation files (the "Software"), to deal 13 | in the Software without restriction, including without limitation the rights 14 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | copies of the Software, and to permit persons to whom the Software is 16 | furnished to do so, subject to the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be included in all 19 | copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | SOFTWARE. 28 | ``` 29 | 30 | However, when building with `-tags gpl`, the produced executable shall be distributed under GPLv3. 31 | -------------------------------------------------------------------------------- /pkg/core/stream.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/lucas-clemente/quic-go" 8 | ) 9 | 10 | // Handle stream close properly 11 | // Ref: https://github.com/libp2p/go-libp2p-quic-transport/blob/master/stream.go 12 | type wrappedQUICStream struct { 13 | Stream quic.Stream 14 | } 15 | 16 | func (s *wrappedQUICStream) StreamID() quic.StreamID { 17 | return s.Stream.StreamID() 18 | } 19 | 20 | func (s *wrappedQUICStream) Read(p []byte) (n int, err error) { 21 | return s.Stream.Read(p) 22 | } 23 | 24 | func (s *wrappedQUICStream) CancelRead(code quic.StreamErrorCode) { 25 | s.Stream.CancelRead(code) 26 | } 27 | 28 | func (s *wrappedQUICStream) SetReadDeadline(t time.Time) error { 29 | return s.Stream.SetReadDeadline(t) 30 | } 31 | 32 | func (s *wrappedQUICStream) Write(p []byte) (n int, err error) { 33 | return s.Stream.Write(p) 34 | } 35 | 36 | func (s *wrappedQUICStream) Close() error { 37 | s.Stream.CancelRead(0) 38 | return s.Stream.Close() 39 | } 40 | 41 | func (s *wrappedQUICStream) CancelWrite(code quic.StreamErrorCode) { 42 | s.Stream.CancelWrite(code) 43 | } 44 | 45 | func (s *wrappedQUICStream) Context() context.Context { 46 | return s.Stream.Context() 47 | } 48 | 49 | func (s *wrappedQUICStream) SetWriteDeadline(t time.Time) error { 50 | return s.Stream.SetWriteDeadline(t) 51 | } 52 | 53 | func (s *wrappedQUICStream) SetDeadline(t time.Time) error { 54 | return s.Stream.SetDeadline(t) 55 | } 56 | -------------------------------------------------------------------------------- /cmd/client_nongpl.go: -------------------------------------------------------------------------------- 1 | //go:build !gpl 2 | // +build !gpl 3 | 4 | package main 5 | 6 | import ( 7 | "github.com/HyNetwork/hysteria/pkg/core" 8 | "github.com/sirupsen/logrus" 9 | ) 10 | 11 | const license = `The MIT License (MIT) 12 | 13 | Copyright (c) 2021 Toby 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy 16 | of this software and associated documentation files (the "Software"), to deal 17 | in the Software without restriction, including without limitation the rights 18 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | copies of the Software, and to permit persons to whom the Software is 20 | furnished to do so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in all 23 | copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 31 | SOFTWARE. 32 | ` 33 | 34 | func startTUN(config *clientConfig, client *core.Client, errChan chan error) { 35 | logrus.Fatalln("TUN mode is only available in GPL builds. Please rebuild hysteria with -tags gpl") 36 | } 37 | -------------------------------------------------------------------------------- /pkg/relay/tcp.go: -------------------------------------------------------------------------------- 1 | package relay 2 | 3 | import ( 4 | "net" 5 | "time" 6 | 7 | "github.com/HyNetwork/hysteria/pkg/core" 8 | "github.com/HyNetwork/hysteria/pkg/utils" 9 | ) 10 | 11 | type TCPRelay struct { 12 | HyClient *core.Client 13 | ListenAddr *net.TCPAddr 14 | Remote string 15 | Timeout time.Duration 16 | 17 | ConnFunc func(addr net.Addr) 18 | ErrorFunc func(addr net.Addr, err error) 19 | } 20 | 21 | func NewTCPRelay(hyClient *core.Client, listen, remote string, timeout time.Duration, 22 | connFunc func(addr net.Addr), errorFunc func(addr net.Addr, err error), 23 | ) (*TCPRelay, error) { 24 | tAddr, err := net.ResolveTCPAddr("tcp", listen) 25 | if err != nil { 26 | return nil, err 27 | } 28 | r := &TCPRelay{ 29 | HyClient: hyClient, 30 | ListenAddr: tAddr, 31 | Remote: remote, 32 | Timeout: timeout, 33 | ConnFunc: connFunc, 34 | ErrorFunc: errorFunc, 35 | } 36 | return r, nil 37 | } 38 | 39 | func (r *TCPRelay) ListenAndServe() error { 40 | listener, err := net.ListenTCP("tcp", r.ListenAddr) 41 | if err != nil { 42 | return err 43 | } 44 | defer listener.Close() 45 | for { 46 | c, err := listener.AcceptTCP() 47 | if err != nil { 48 | return err 49 | } 50 | go func() { 51 | defer c.Close() 52 | r.ConnFunc(c.RemoteAddr()) 53 | rc, err := r.HyClient.DialTCP(r.Remote) 54 | if err != nil { 55 | r.ErrorFunc(c.RemoteAddr(), err) 56 | return 57 | } 58 | defer rc.Close() 59 | err = utils.PipePairWithTimeout(c, rc, r.Timeout) 60 | r.ErrorFunc(c.RemoteAddr(), err) 61 | }() 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **For Matsuri plugin build. Use MIT License. No TUN support.** 2 | 3 | # ![Logo](docs/logos/readme.png) 4 | 5 | [![License][1]][2] [![Release][3]][4] [![Telegram][5]][6] [![Discussions][7]][8] 6 | 7 | [1]: https://img.shields.io/badge/license-MIT-blue 8 | 9 | [2]: LICENSE.md 10 | 11 | [3]: https://img.shields.io/github/v/release/HyNetwork/hysteria?style=flat-square 12 | 13 | [4]: https://github.com/HyNetwork/hysteria/releases 14 | 15 | [5]: https://img.shields.io/badge/chat-Telegram-blue?style=flat-square 16 | 17 | [6]: https://t.me/hysteria_github 18 | 19 | [7]: https://img.shields.io/github/discussions/HyNetwork/hysteria?style=flat-square 20 | 21 | [8]: https://github.com/HyNetwork/hysteria/discussions 22 | 23 | Hysteria is a feature-packed proxy & relay utility optimized for lossy, unstable connections (e.g. satellite networks, 24 | congested public Wi-Fi, connecting from China to servers abroad) powered by a customized QUIC protocol. 25 | 26 | - SOCKS5 proxy (TCP & UDP) 27 | - HTTP/HTTPS proxy 28 | - TCP/UDP relay 29 | - TCP/UDP TPROXY (Linux) 30 | - TCP REDIRECT (Linux) 31 | - TUN (TAP on Windows) 32 | - Still growing... 33 | 34 | **[See wiki for documentation](https://github.com/HyNetwork/hysteria/wiki)** 35 | 36 | ---------- 37 | 38 | Hysteria 是一个功能丰富的,专为恶劣网络环境进行优化的网络工具(双边加速),比如卫星网络、拥挤的公共 Wi-Fi、在中国连接国外服务器等。 基于修改版的 QUIC 协议。 39 | 40 | - SOCKS5 代理 (TCP & UDP) 41 | - HTTP/HTTPS 代理 42 | - TCP/UDP 转发 43 | - TCP/UDP TPROXY 透明代理 (Linux) 44 | - TCP REDIRECT 透明代理 (Linux) 45 | - TUN (Windows 下为 TAP) 46 | - 仍在增加中... 47 | 48 | **[文档请见 wiki](https://github.com/HyNetwork/hysteria/wiki/%E9%A6%96%E9%A1%B5)** 49 | 50 | ---------- 51 | 52 | ![Bench](docs/bench/bench.png) 53 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build and release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | 10 | build: 11 | name: Build and release 12 | runs-on: ubuntu-latest 13 | env: 14 | ACTIONS_ALLOW_UNSECURE_COMMANDS: true 15 | 16 | steps: 17 | 18 | - name: Check out 19 | uses: actions/checkout@v3 20 | 21 | - name: Get tag 22 | uses: olegtarasov/get-tag@v2 23 | id: tagName 24 | 25 | - name: Get time 26 | uses: gerred/actions/current-time@master 27 | id: current-time 28 | 29 | - name: Build 30 | uses: tobyxdd/go-cross-build@d00fc41eb205f57dd90f6e5af4613e21c7ebe73f 31 | env: 32 | TIME: "${{ steps.current-time.outputs.time }}" 33 | CGO_ENABLED: "0" 34 | with: 35 | name: hysteria 36 | dest: dist 37 | ldflags: -w -s -X main.appVersion=${{ env.GIT_TAG_NAME }} -X main.appCommit=${{ github.sha }} -X main.appDate=${{ env.TIME }} 38 | platforms: 'linux/amd64, linux/386, linux/arm, linux/arm64' 39 | package: cmd 40 | compress: false 41 | 42 | - name: Generate hashes 43 | run: | 44 | cd dist 45 | for f in $(find . -type f); do 46 | sha256sum $f | sudo tee -a hashes.txt 47 | done 48 | 49 | - name: Upload 50 | uses: softprops/action-gh-release@v1 51 | if: startsWith(github.ref, 'refs/tags/') 52 | with: 53 | files: | 54 | ./dist/hysteria-linux-amd64 55 | ./dist/hysteria-linux-386 56 | ./dist/hysteria-linux-arm 57 | ./dist/hysteria-linux-arm64 58 | ./dist/hashes.txt 59 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.19-alpine AS builder 2 | 3 | LABEL maintainer="mritd " 4 | 5 | # GOPROXY is disabled by default, use: 6 | # docker build --build-arg GOPROXY="https://goproxy.io" ... 7 | # to enable GOPROXY. 8 | ARG GOPROXY="" 9 | 10 | ENV GOPROXY ${GOPROXY} 11 | 12 | COPY . /go/src/github.com/hynetwork/hysteria 13 | 14 | WORKDIR /go/src/github.com/hynetwork/hysteria/cmd 15 | 16 | RUN set -ex \ 17 | && apk add git build-base \ 18 | && export VERSION=$(git describe --tags) \ 19 | && export COMMIT=$(git rev-parse HEAD) \ 20 | && export TIMESTAMP=$(date "+%F %T") \ 21 | && go build -trimpath -o /go/bin/hysteria -ldflags \ 22 | "-w -s -X 'main.appVersion=${VERSION}' \ 23 | -X 'main.appCommit=${COMMIT}' \ 24 | -X 'main.appDate=${TIMESTAMP}'" 25 | 26 | # multi-stage builds to create the final image 27 | FROM alpine AS dist 28 | 29 | LABEL maintainer="mritd " 30 | 31 | # set up nsswitch.conf for Go's "netgo" implementation 32 | # - https://github.com/golang/go/blob/go1.9.1/src/net/conf.go#L194-L275 33 | # - docker run --rm debian:stretch grep '^hosts:' /etc/nsswitch.conf 34 | RUN [ ! -e /etc/nsswitch.conf ] && echo 'hosts: files dns' > /etc/nsswitch.conf 35 | 36 | # bash is used for debugging, tzdata is used to add timezone information. 37 | # Install ca-certificates to ensure no CA certificate errors. 38 | # 39 | # Do not try to add the "--no-cache" option when there are multiple "apk" 40 | # commands, this will cause the build process to become very slow. 41 | RUN set -ex \ 42 | && apk upgrade \ 43 | && apk add bash tzdata ca-certificates \ 44 | && rm -rf /var/cache/apk/* 45 | 46 | COPY --from=builder /go/bin/hysteria /usr/local/bin/hysteria 47 | 48 | ENTRYPOINT ["hysteria"] 49 | -------------------------------------------------------------------------------- /pkg/core/frag.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | func fragUDPMessage(m udpMessage, maxSize int) []udpMessage { 4 | if m.Size() <= maxSize { 5 | return []udpMessage{m} 6 | } 7 | fullPayload := m.Data 8 | maxPayloadSize := maxSize - m.HeaderSize() 9 | off := 0 10 | fragID := uint8(0) 11 | fragCount := uint8((len(fullPayload) + maxPayloadSize - 1) / maxPayloadSize) // round up 12 | var frags []udpMessage 13 | for off < len(fullPayload) { 14 | payloadSize := len(fullPayload) - off 15 | if payloadSize > maxPayloadSize { 16 | payloadSize = maxPayloadSize 17 | } 18 | frag := m 19 | frag.FragID = fragID 20 | frag.FragCount = fragCount 21 | frag.DataLen = uint16(payloadSize) 22 | frag.Data = fullPayload[off : off+payloadSize] 23 | frags = append(frags, frag) 24 | off += payloadSize 25 | fragID++ 26 | } 27 | return frags 28 | } 29 | 30 | type defragger struct { 31 | msgID uint16 32 | frags []*udpMessage 33 | count uint8 34 | } 35 | 36 | func (d *defragger) Feed(m udpMessage) *udpMessage { 37 | if m.FragCount <= 1 { 38 | return &m 39 | } 40 | if m.FragID >= m.FragCount { 41 | // wtf is this? 42 | return nil 43 | } 44 | if m.MsgID != d.msgID { 45 | // new message, clear previous state 46 | d.msgID = m.MsgID 47 | d.frags = make([]*udpMessage, m.FragCount) 48 | d.count = 1 49 | d.frags[m.FragID] = &m 50 | } else if d.frags[m.FragID] == nil { 51 | d.frags[m.FragID] = &m 52 | d.count++ 53 | if int(d.count) == len(d.frags) { 54 | // all fragments received, assemble 55 | var data []byte 56 | for _, frag := range d.frags { 57 | data = append(data, frag.Data...) 58 | } 59 | m.DataLen = uint16(len(data)) 60 | m.Data = data 61 | m.FragID = 0 62 | m.FragCount = 1 63 | return &m 64 | } 65 | } 66 | return nil 67 | } 68 | -------------------------------------------------------------------------------- /pkg/core/protocol.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | const ( 8 | protocolVersion = uint8(3) 9 | protocolVersionV2 = uint8(2) 10 | protocolTimeout = 10 * time.Second 11 | 12 | closeErrorCodeGeneric = 0 13 | closeErrorCodeProtocol = 1 14 | closeErrorCodeAuth = 2 15 | ) 16 | 17 | type transmissionRate struct { 18 | SendBPS uint64 19 | RecvBPS uint64 20 | } 21 | 22 | type clientHello struct { 23 | Rate transmissionRate 24 | AuthLen uint16 `struc:"sizeof=Auth"` 25 | Auth []byte 26 | } 27 | 28 | type serverHello struct { 29 | OK bool 30 | Rate transmissionRate 31 | MessageLen uint16 `struc:"sizeof=Message"` 32 | Message string 33 | } 34 | 35 | type clientRequest struct { 36 | UDP bool 37 | HostLen uint16 `struc:"sizeof=Host"` 38 | Host string 39 | Port uint16 40 | } 41 | 42 | type serverResponse struct { 43 | OK bool 44 | UDPSessionID uint32 45 | MessageLen uint16 `struc:"sizeof=Message"` 46 | Message string 47 | } 48 | 49 | type udpMessage struct { 50 | SessionID uint32 51 | HostLen uint16 `struc:"sizeof=Host"` 52 | Host string 53 | Port uint16 54 | MsgID uint16 // doesn't matter when not fragmented, but must not be 0 when fragmented 55 | FragID uint8 // doesn't matter when not fragmented, starts at 0 when fragmented 56 | FragCount uint8 // must be 1 when not fragmented 57 | DataLen uint16 `struc:"sizeof=Data"` 58 | Data []byte 59 | } 60 | 61 | func (m udpMessage) HeaderSize() int { 62 | return 4 + 2 + len(m.Host) + 2 + 2 + 1 + 1 + 2 63 | } 64 | 65 | func (m udpMessage) Size() int { 66 | return m.HeaderSize() + len(m.Data) 67 | } 68 | 69 | type udpMessageV2 struct { 70 | SessionID uint32 71 | HostLen uint16 `struc:"sizeof=Host"` 72 | Host string 73 | Port uint16 74 | DataLen uint16 `struc:"sizeof=Data"` 75 | Data []byte 76 | } 77 | -------------------------------------------------------------------------------- /pkg/tproxy/tcp_linux.go: -------------------------------------------------------------------------------- 1 | package tproxy 2 | 3 | import ( 4 | "net" 5 | "time" 6 | 7 | "github.com/HyNetwork/hysteria/pkg/core" 8 | "github.com/HyNetwork/hysteria/pkg/utils" 9 | "github.com/LiamHaworth/go-tproxy" 10 | ) 11 | 12 | type TCPTProxy struct { 13 | HyClient *core.Client 14 | ListenAddr *net.TCPAddr 15 | Timeout time.Duration 16 | 17 | ConnFunc func(addr, reqAddr net.Addr) 18 | ErrorFunc func(addr, reqAddr net.Addr, err error) 19 | } 20 | 21 | func NewTCPTProxy(hyClient *core.Client, listen string, timeout time.Duration, 22 | connFunc func(addr, reqAddr net.Addr), 23 | errorFunc func(addr, reqAddr net.Addr, err error), 24 | ) (*TCPTProxy, error) { 25 | tAddr, err := net.ResolveTCPAddr("tcp", listen) 26 | if err != nil { 27 | return nil, err 28 | } 29 | r := &TCPTProxy{ 30 | HyClient: hyClient, 31 | ListenAddr: tAddr, 32 | Timeout: timeout, 33 | ConnFunc: connFunc, 34 | ErrorFunc: errorFunc, 35 | } 36 | return r, nil 37 | } 38 | 39 | func (r *TCPTProxy) ListenAndServe() error { 40 | listener, err := tproxy.ListenTCP("tcp", r.ListenAddr) 41 | if err != nil { 42 | return err 43 | } 44 | defer listener.Close() 45 | for { 46 | c, err := listener.Accept() 47 | if err != nil { 48 | return err 49 | } 50 | go func() { 51 | defer c.Close() 52 | // Under TPROXY mode, we are effectively acting as the remote server 53 | // So our LocalAddr is actually the target to which the user is trying to connect 54 | // and our RemoteAddr is the local address where the user initiates the connection 55 | r.ConnFunc(c.RemoteAddr(), c.LocalAddr()) 56 | rc, err := r.HyClient.DialTCP(c.LocalAddr().String()) 57 | if err != nil { 58 | r.ErrorFunc(c.RemoteAddr(), c.LocalAddr(), err) 59 | return 60 | } 61 | defer rc.Close() 62 | err = utils.PipePairWithTimeout(c, rc, r.Timeout) 63 | r.ErrorFunc(c.RemoteAddr(), c.LocalAddr(), err) 64 | }() 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /cmd/auth/http.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io/ioutil" 7 | "net" 8 | "net/http" 9 | 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | type HTTPAuthProvider struct { 14 | Client *http.Client 15 | URL string 16 | } 17 | 18 | type authReq struct { 19 | Addr string `json:"addr"` 20 | Payload []byte `json:"payload"` 21 | Send uint64 `json:"send"` 22 | Recv uint64 `json:"recv"` 23 | } 24 | 25 | type authResp struct { 26 | OK bool `json:"ok"` 27 | Msg string `json:"msg"` 28 | } 29 | 30 | func (p *HTTPAuthProvider) Auth(addr net.Addr, auth []byte, sSend uint64, sRecv uint64) (bool, string) { 31 | jbs, err := json.Marshal(&authReq{ 32 | Addr: addr.String(), 33 | Payload: auth, 34 | Send: sSend, 35 | Recv: sRecv, 36 | }) 37 | if err != nil { 38 | logrus.WithFields(logrus.Fields{ 39 | "error": err, 40 | }).Error("Failed to marshal auth request") 41 | return false, "internal error" 42 | } 43 | resp, err := p.Client.Post(p.URL, "application/json", bytes.NewBuffer(jbs)) 44 | if err != nil { 45 | logrus.WithFields(logrus.Fields{ 46 | "error": err, 47 | }).Error("Failed to send auth request") 48 | return false, "internal error" 49 | } 50 | defer resp.Body.Close() 51 | if resp.StatusCode != http.StatusOK { 52 | logrus.WithFields(logrus.Fields{ 53 | "code": resp.StatusCode, 54 | }).Error("Invalid status code from auth server") 55 | return false, "internal error" 56 | } 57 | data, err := ioutil.ReadAll(resp.Body) 58 | if err != nil { 59 | logrus.WithFields(logrus.Fields{ 60 | "error": err, 61 | }).Error("Failed to read auth response") 62 | return false, "internal error" 63 | } 64 | var ar authResp 65 | err = json.Unmarshal(data, &ar) 66 | if err != nil { 67 | logrus.WithFields(logrus.Fields{ 68 | "error": err, 69 | }).Error("Failed to unmarshal auth response") 70 | return false, "internal error" 71 | } 72 | return ar.OK, ar.Msg 73 | } 74 | -------------------------------------------------------------------------------- /cmd/completion.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var completionCmd = &cobra.Command{ 11 | Use: "completion [bash|zsh|fish|powershell]", 12 | Short: "Generate completion script", 13 | Long: fmt.Sprintf(`To load completions: 14 | 15 | Bash: 16 | 17 | $ source <(%[1]s completion bash) 18 | 19 | # To load completions for each session, execute once: 20 | # Linux: 21 | $ %[1]s completion bash > /etc/bash_completion.d/%[1]s 22 | # macOS: 23 | $ %[1]s completion bash > /usr/local/etc/bash_completion.d/%[1]s 24 | 25 | Zsh: 26 | 27 | # If shell completion is not already enabled in your environment, 28 | # you will need to enable it. You can execute the following once: 29 | 30 | $ echo "autoload -U compinit; compinit" >> ~/.zshrc 31 | 32 | # To load completions for each session, execute once: 33 | $ %[1]s completion zsh > "${fpath[1]}/_%[1]s" 34 | 35 | # You will need to start a new shell for this setup to take effect. 36 | 37 | fish: 38 | 39 | $ %[1]s completion fish | source 40 | 41 | # To load completions for each session, execute once: 42 | $ %[1]s completion fish > ~/.config/fish/completions/%[1]s.fish 43 | 44 | PowerShell: 45 | 46 | PS> %[1]s completion powershell | Out-String | Invoke-Expression 47 | 48 | # To load completions for every new session, run: 49 | PS> %[1]s completion powershell > %[1]s.ps1 50 | # and source this file from your PowerShell profile. 51 | `, rootCmd.Name()), 52 | DisableFlagsInUseLine: true, 53 | ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, 54 | Args: cobra.ExactValidArgs(1), 55 | Run: func(cmd *cobra.Command, args []string) { 56 | switch args[0] { 57 | case "bash": 58 | _ = cmd.Root().GenBashCompletion(os.Stdout) 59 | case "zsh": 60 | _ = cmd.Root().GenZshCompletion(os.Stdout) 61 | case "fish": 62 | _ = cmd.Root().GenFishCompletion(os.Stdout, true) 63 | case "powershell": 64 | _ = cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout) 65 | } 66 | }, 67 | } 68 | -------------------------------------------------------------------------------- /pkg/utils/pipe.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "io" 5 | "net" 6 | "time" 7 | ) 8 | 9 | const PipeBufferSize = 65535 10 | 11 | func Pipe(src, dst io.ReadWriter, count func(int)) error { 12 | buf := make([]byte, PipeBufferSize) 13 | for { 14 | rn, err := src.Read(buf) 15 | if rn > 0 { 16 | if count != nil { 17 | count(rn) 18 | } 19 | _, err := dst.Write(buf[:rn]) 20 | if err != nil { 21 | return err 22 | } 23 | } 24 | if err != nil { 25 | return err 26 | } 27 | } 28 | } 29 | 30 | // count: positive numbers for rw1 to rw2, negative numbers for rw2 to re1 31 | func Pipe2Way(rw1, rw2 io.ReadWriter, count func(int)) error { 32 | errChan := make(chan error, 2) 33 | go func() { 34 | var revCount func(int) 35 | if count != nil { 36 | revCount = func(i int) { 37 | count(-i) 38 | } 39 | } 40 | errChan <- Pipe(rw2, rw1, revCount) 41 | }() 42 | go func() { 43 | errChan <- Pipe(rw1, rw2, count) 44 | }() 45 | // We only need the first error 46 | return <-errChan 47 | } 48 | 49 | func PipePairWithTimeout(conn net.Conn, stream io.ReadWriteCloser, timeout time.Duration) error { 50 | errChan := make(chan error, 2) 51 | // TCP to stream 52 | go func() { 53 | buf := make([]byte, PipeBufferSize) 54 | for { 55 | if timeout != 0 { 56 | _ = conn.SetDeadline(time.Now().Add(timeout)) 57 | } 58 | rn, err := conn.Read(buf) 59 | if rn > 0 { 60 | _, err := stream.Write(buf[:rn]) 61 | if err != nil { 62 | errChan <- err 63 | return 64 | } 65 | } 66 | if err != nil { 67 | errChan <- err 68 | return 69 | } 70 | } 71 | }() 72 | // Stream to TCP 73 | go func() { 74 | buf := make([]byte, PipeBufferSize) 75 | for { 76 | rn, err := stream.Read(buf) 77 | if rn > 0 { 78 | _, err := conn.Write(buf[:rn]) 79 | if err != nil { 80 | errChan <- err 81 | return 82 | } 83 | if timeout != 0 { 84 | _ = conn.SetDeadline(time.Now().Add(timeout)) 85 | } 86 | } 87 | if err != nil { 88 | errChan <- err 89 | return 90 | } 91 | } 92 | }() 93 | return <-errChan 94 | } 95 | -------------------------------------------------------------------------------- /pkg/conns/faketcp/obfs.go: -------------------------------------------------------------------------------- 1 | package faketcp 2 | 3 | import ( 4 | "net" 5 | "sync" 6 | "syscall" 7 | "time" 8 | 9 | "github.com/HyNetwork/hysteria/pkg/obfs" 10 | ) 11 | 12 | const udpBufferSize = 65535 13 | 14 | type ObfsFakeTCPConn struct { 15 | orig *TCPConn 16 | obfs obfs.Obfuscator 17 | 18 | readBuf []byte 19 | readMutex sync.Mutex 20 | writeBuf []byte 21 | writeMutex sync.Mutex 22 | } 23 | 24 | func NewObfsFakeTCPConn(orig *TCPConn, obfs obfs.Obfuscator) *ObfsFakeTCPConn { 25 | return &ObfsFakeTCPConn{ 26 | orig: orig, 27 | obfs: obfs, 28 | readBuf: make([]byte, udpBufferSize), 29 | writeBuf: make([]byte, udpBufferSize), 30 | } 31 | } 32 | 33 | func (c *ObfsFakeTCPConn) ReadFrom(p []byte) (int, net.Addr, error) { 34 | for { 35 | c.readMutex.Lock() 36 | n, addr, err := c.orig.ReadFrom(c.readBuf) 37 | if n <= 0 { 38 | c.readMutex.Unlock() 39 | return 0, addr, err 40 | } 41 | newN := c.obfs.Deobfuscate(c.readBuf[:n], p) 42 | c.readMutex.Unlock() 43 | if newN > 0 { 44 | // Valid packet 45 | return newN, addr, err 46 | } else if err != nil { 47 | // Not valid and orig.ReadFrom had some error 48 | return 0, addr, err 49 | } 50 | } 51 | } 52 | 53 | func (c *ObfsFakeTCPConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { 54 | c.writeMutex.Lock() 55 | bn := c.obfs.Obfuscate(p, c.writeBuf) 56 | _, err = c.orig.WriteTo(c.writeBuf[:bn], addr) 57 | c.writeMutex.Unlock() 58 | if err != nil { 59 | return 0, err 60 | } else { 61 | return len(p), nil 62 | } 63 | } 64 | 65 | func (c *ObfsFakeTCPConn) Close() error { 66 | return c.orig.Close() 67 | } 68 | 69 | func (c *ObfsFakeTCPConn) LocalAddr() net.Addr { 70 | return c.orig.LocalAddr() 71 | } 72 | 73 | func (c *ObfsFakeTCPConn) SetDeadline(t time.Time) error { 74 | return c.orig.SetDeadline(t) 75 | } 76 | 77 | func (c *ObfsFakeTCPConn) SetReadDeadline(t time.Time) error { 78 | return c.orig.SetReadDeadline(t) 79 | } 80 | 81 | func (c *ObfsFakeTCPConn) SetWriteDeadline(t time.Time) error { 82 | return c.orig.SetWriteDeadline(t) 83 | } 84 | 85 | func (c *ObfsFakeTCPConn) SetReadBuffer(bytes int) error { 86 | return c.orig.SetReadBuffer(bytes) 87 | } 88 | 89 | func (c *ObfsFakeTCPConn) SetWriteBuffer(bytes int) error { 90 | return c.orig.SetWriteBuffer(bytes) 91 | } 92 | 93 | func (c *ObfsFakeTCPConn) SyscallConn() (syscall.RawConn, error) { 94 | return c.orig.SyscallConn() 95 | } 96 | -------------------------------------------------------------------------------- /pkg/transport/resolve.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net" 8 | "time" 9 | ) 10 | 11 | type ResolvePreference int 12 | 13 | const ( 14 | ResolvePreferenceDefault = ResolvePreference(iota) 15 | ResolvePreferenceIPv4 16 | ResolvePreferenceIPv6 17 | ResolvePreferenceIPv4OrIPv6 18 | ResolvePreferenceIPv6OrIPv4 19 | 20 | ResolveTimeout = 8 * time.Second 21 | ) 22 | 23 | var ( 24 | errNoIPv4Addr = errors.New("no IPv4 address") 25 | errNoIPv6Addr = errors.New("no IPv6 address") 26 | errNoAddr = errors.New("no address") 27 | ) 28 | 29 | func resolveIPAddrWithPreference(host string, pref ResolvePreference) (*net.IPAddr, error) { 30 | if pref == ResolvePreferenceDefault { 31 | return net.ResolveIPAddr("ip", host) 32 | } 33 | ctx, cancel := context.WithTimeout(context.Background(), ResolveTimeout) 34 | ips, err := net.DefaultResolver.LookupIPAddr(ctx, host) 35 | cancel() 36 | if err != nil { 37 | return nil, err 38 | } 39 | var ip4, ip6 *net.IPAddr 40 | for i := range ips { 41 | ip := &ips[i] 42 | is4 := ip.IP.To4() != nil 43 | if ip4 == nil && is4 { 44 | ip4 = ip 45 | } else if ip6 == nil && !is4 { 46 | ip6 = ip 47 | } 48 | if ip4 != nil && ip6 != nil { 49 | break 50 | } 51 | } 52 | switch pref { 53 | case ResolvePreferenceIPv4: 54 | if ip4 == nil { 55 | return nil, errNoIPv4Addr 56 | } 57 | return ip4, nil 58 | case ResolvePreferenceIPv6: 59 | if ip6 == nil { 60 | return nil, errNoIPv6Addr 61 | } 62 | return ip6, nil 63 | case ResolvePreferenceIPv4OrIPv6: 64 | if ip4 == nil { 65 | if ip6 == nil { 66 | return nil, errNoAddr 67 | } else { 68 | return ip6, nil 69 | } 70 | } 71 | return ip4, nil 72 | case ResolvePreferenceIPv6OrIPv4: 73 | if ip6 == nil { 74 | if ip4 == nil { 75 | return nil, errNoAddr 76 | } else { 77 | return ip4, nil 78 | } 79 | } 80 | return ip6, nil 81 | } 82 | return nil, errNoAddr 83 | } 84 | 85 | func ResolvePreferenceFromString(preference string) (ResolvePreference, error) { 86 | switch preference { 87 | case "4": 88 | return ResolvePreferenceIPv4, nil 89 | case "6": 90 | return ResolvePreferenceIPv6, nil 91 | case "46": 92 | return ResolvePreferenceIPv4OrIPv6, nil 93 | case "64": 94 | return ResolvePreferenceIPv6OrIPv4, nil 95 | default: 96 | return ResolvePreferenceDefault, fmt.Errorf("invalid preference: %s", preference) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /pkg/acl/entry_test.go: -------------------------------------------------------------------------------- 1 | package acl 2 | 3 | import ( 4 | "net" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestParseEntry(t *testing.T) { 10 | _, ok3net, _ := net.ParseCIDR("8.8.8.0/24") 11 | 12 | type args struct { 13 | s string 14 | } 15 | tests := []struct { 16 | name string 17 | args args 18 | want Entry 19 | wantErr bool 20 | }{ 21 | {name: "empty", args: args{""}, want: Entry{}, wantErr: true}, 22 | { 23 | name: "ok 1", args: args{"direct domain-suffix google.com"}, 24 | want: Entry{ActionDirect, "", &domainMatcher{ 25 | matcherBase: matcherBase{}, 26 | Domain: "google.com", 27 | Suffix: true, 28 | }}, 29 | wantErr: false, 30 | }, 31 | { 32 | name: "ok 2", args: args{"proxy domain shithole"}, 33 | want: Entry{ActionProxy, "", &domainMatcher{ 34 | matcherBase: matcherBase{}, 35 | Domain: "shithole", 36 | Suffix: false, 37 | }}, 38 | wantErr: false, 39 | }, 40 | { 41 | name: "ok 3", args: args{"block cidr 8.8.8.0/24 */53"}, 42 | want: Entry{ActionBlock, "", &netMatcher{ 43 | matcherBase: matcherBase{ProtocolAll, 53}, 44 | Net: ok3net, 45 | }}, 46 | wantErr: false, 47 | }, 48 | { 49 | name: "ok 4", args: args{"hijack all udp/* udpblackhole.net"}, 50 | want: Entry{ActionHijack, "udpblackhole.net", &allMatcher{ 51 | matcherBase: matcherBase{ProtocolUDP, 0}, 52 | }}, 53 | wantErr: false, 54 | }, 55 | { 56 | name: "err 1", args: args{"what the heck"}, 57 | want: Entry{}, 58 | wantErr: true, 59 | }, 60 | { 61 | name: "err 2", args: args{"proxy sucks ass"}, 62 | want: Entry{}, 63 | wantErr: true, 64 | }, 65 | { 66 | name: "err 3", args: args{"block ip 999.999.999.999"}, 67 | want: Entry{}, 68 | wantErr: true, 69 | }, 70 | { 71 | name: "err 4", args: args{"hijack domain google.com"}, 72 | want: Entry{}, 73 | wantErr: true, 74 | }, 75 | { 76 | name: "err 5", args: args{"hijack domain google.com bing.com 123"}, 77 | want: Entry{}, 78 | wantErr: true, 79 | }, 80 | } 81 | for _, tt := range tests { 82 | t.Run(tt.name, func(t *testing.T) { 83 | got, err := ParseEntry(tt.args.s) 84 | if (err != nil) != tt.wantErr { 85 | t.Errorf("ParseEntry() error = %v, wantErr %v", err, tt.wantErr) 86 | return 87 | } 88 | if !reflect.DeepEqual(got, tt.want) { 89 | t.Errorf("ParseEntry() got = %v, wantAction %v", got, tt.want) 90 | } 91 | }) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /pkg/congestion/pacer.go: -------------------------------------------------------------------------------- 1 | package congestion 2 | 3 | import ( 4 | "math" 5 | "time" 6 | 7 | "github.com/lucas-clemente/quic-go/congestion" 8 | ) 9 | 10 | const ( 11 | maxBurstPackets = 10 12 | minPacingDelay = time.Millisecond 13 | ) 14 | 15 | // The pacer implements a token bucket pacing algorithm. 16 | type pacer struct { 17 | budgetAtLastSent congestion.ByteCount 18 | maxDatagramSize congestion.ByteCount 19 | lastSentTime time.Time 20 | getBandwidth func() congestion.ByteCount // in bytes/s 21 | } 22 | 23 | func newPacer(getBandwidth func() congestion.ByteCount) *pacer { 24 | p := &pacer{ 25 | budgetAtLastSent: maxBurstPackets * initMaxDatagramSize, 26 | maxDatagramSize: initMaxDatagramSize, 27 | getBandwidth: getBandwidth, 28 | } 29 | return p 30 | } 31 | 32 | func (p *pacer) SentPacket(sendTime time.Time, size congestion.ByteCount) { 33 | budget := p.Budget(sendTime) 34 | if size > budget { 35 | p.budgetAtLastSent = 0 36 | } else { 37 | p.budgetAtLastSent = budget - size 38 | } 39 | p.lastSentTime = sendTime 40 | } 41 | 42 | func (p *pacer) Budget(now time.Time) congestion.ByteCount { 43 | if p.lastSentTime.IsZero() { 44 | return p.maxBurstSize() 45 | } 46 | budget := p.budgetAtLastSent + (p.getBandwidth()*congestion.ByteCount(now.Sub(p.lastSentTime).Nanoseconds()))/1e9 47 | return minByteCount(p.maxBurstSize(), budget) 48 | } 49 | 50 | func (p *pacer) maxBurstSize() congestion.ByteCount { 51 | return maxByteCount( 52 | congestion.ByteCount((minPacingDelay+time.Millisecond).Nanoseconds())*p.getBandwidth()/1e9, 53 | maxBurstPackets*p.maxDatagramSize, 54 | ) 55 | } 56 | 57 | // TimeUntilSend returns when the next packet should be sent. 58 | // It returns the zero value of time.Time if a packet can be sent immediately. 59 | func (p *pacer) TimeUntilSend() time.Time { 60 | if p.budgetAtLastSent >= p.maxDatagramSize { 61 | return time.Time{} 62 | } 63 | return p.lastSentTime.Add(maxDuration( 64 | minPacingDelay, 65 | time.Duration(math.Ceil(float64(p.maxDatagramSize-p.budgetAtLastSent)*1e9/ 66 | float64(p.getBandwidth())))*time.Nanosecond, 67 | )) 68 | } 69 | 70 | func (p *pacer) SetMaxDatagramSize(s congestion.ByteCount) { 71 | p.maxDatagramSize = s 72 | } 73 | 74 | func maxByteCount(a, b congestion.ByteCount) congestion.ByteCount { 75 | if a < b { 76 | return b 77 | } 78 | return a 79 | } 80 | 81 | func minByteCount(a, b congestion.ByteCount) congestion.ByteCount { 82 | if a < b { 83 | return a 84 | } 85 | return b 86 | } 87 | -------------------------------------------------------------------------------- /cmd/kploader.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "sync" 6 | 7 | "github.com/fsnotify/fsnotify" 8 | "github.com/sirupsen/logrus" 9 | ) 10 | 11 | type keypairLoader struct { 12 | certMu sync.RWMutex 13 | cert *tls.Certificate 14 | certPath string 15 | keyPath string 16 | } 17 | 18 | func newKeypairLoader(certPath, keyPath string) (*keypairLoader, error) { 19 | loader := &keypairLoader{ 20 | certPath: certPath, 21 | keyPath: keyPath, 22 | } 23 | cert, err := tls.LoadX509KeyPair(certPath, keyPath) 24 | if err != nil { 25 | return nil, err 26 | } 27 | loader.cert = &cert 28 | watcher, err := fsnotify.NewWatcher() 29 | if err != nil { 30 | return nil, err 31 | } 32 | go func() { 33 | for { 34 | select { 35 | case event, ok := <-watcher.Events: 36 | if !ok { 37 | return 38 | } 39 | switch event.Op { 40 | case fsnotify.Create, fsnotify.Write, fsnotify.Rename, fsnotify.Chmod: 41 | logrus.WithFields(logrus.Fields{ 42 | "file": event.Name, 43 | }).Info("Keypair change detected, reloading...") 44 | if err := loader.load(); err != nil { 45 | logrus.WithFields(logrus.Fields{ 46 | "error": err, 47 | }).Error("Failed to reload keypair") 48 | } else { 49 | logrus.Info("Keypair successfully reloaded") 50 | } 51 | case fsnotify.Remove: 52 | _ = watcher.Add(event.Name) // Workaround for vim 53 | // https://github.com/fsnotify/fsnotify/issues/92 54 | } 55 | case err, ok := <-watcher.Errors: 56 | if !ok { 57 | return 58 | } 59 | logrus.WithFields(logrus.Fields{ 60 | "error": err, 61 | }).Error("Failed to watch keypair files for changes") 62 | } 63 | } 64 | }() 65 | err = watcher.Add(certPath) 66 | if err != nil { 67 | _ = watcher.Close() 68 | return nil, err 69 | } 70 | err = watcher.Add(keyPath) 71 | if err != nil { 72 | _ = watcher.Close() 73 | return nil, err 74 | } 75 | return loader, nil 76 | } 77 | 78 | func (kpr *keypairLoader) load() error { 79 | cert, err := tls.LoadX509KeyPair(kpr.certPath, kpr.keyPath) 80 | if err != nil { 81 | return err 82 | } 83 | kpr.certMu.Lock() 84 | kpr.cert = &cert 85 | kpr.certMu.Unlock() 86 | return nil 87 | } 88 | 89 | func (kpr *keypairLoader) GetCertificateFunc() func(*tls.ClientHelloInfo) (*tls.Certificate, error) { 90 | return func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { 91 | kpr.certMu.RLock() 92 | defer kpr.certMu.RUnlock() 93 | return kpr.cert, nil 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /pkg/conns/udp/obfs.go: -------------------------------------------------------------------------------- 1 | package udp 2 | 3 | import ( 4 | "net" 5 | "os" 6 | "sync" 7 | "syscall" 8 | "time" 9 | 10 | "github.com/HyNetwork/hysteria/pkg/obfs" 11 | ) 12 | 13 | const udpBufferSize = 65535 14 | 15 | type ObfsUDPConn struct { 16 | orig *net.UDPConn 17 | obfs obfs.Obfuscator 18 | 19 | readBuf []byte 20 | readMutex sync.Mutex 21 | writeBuf []byte 22 | writeMutex sync.Mutex 23 | } 24 | 25 | func NewObfsUDPConn(orig *net.UDPConn, obfs obfs.Obfuscator) *ObfsUDPConn { 26 | return &ObfsUDPConn{ 27 | orig: orig, 28 | obfs: obfs, 29 | readBuf: make([]byte, udpBufferSize), 30 | writeBuf: make([]byte, udpBufferSize), 31 | } 32 | } 33 | 34 | func (c *ObfsUDPConn) ReadFrom(p []byte) (int, net.Addr, error) { 35 | for { 36 | c.readMutex.Lock() 37 | n, addr, err := c.orig.ReadFrom(c.readBuf) 38 | if n <= 0 { 39 | c.readMutex.Unlock() 40 | return 0, addr, err 41 | } 42 | newN := c.obfs.Deobfuscate(c.readBuf[:n], p) 43 | c.readMutex.Unlock() 44 | if newN > 0 { 45 | // Valid packet 46 | return newN, addr, err 47 | } else if err != nil { 48 | // Not valid and orig.ReadFrom had some error 49 | return 0, addr, err 50 | } 51 | } 52 | } 53 | 54 | func (c *ObfsUDPConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { 55 | c.writeMutex.Lock() 56 | bn := c.obfs.Obfuscate(p, c.writeBuf) 57 | _, err = c.orig.WriteTo(c.writeBuf[:bn], addr) 58 | c.writeMutex.Unlock() 59 | if err != nil { 60 | return 0, err 61 | } else { 62 | return len(p), nil 63 | } 64 | } 65 | 66 | func (c *ObfsUDPConn) Close() error { 67 | return c.orig.Close() 68 | } 69 | 70 | func (c *ObfsUDPConn) LocalAddr() net.Addr { 71 | return c.orig.LocalAddr() 72 | } 73 | 74 | func (c *ObfsUDPConn) SetDeadline(t time.Time) error { 75 | return c.orig.SetDeadline(t) 76 | } 77 | 78 | func (c *ObfsUDPConn) SetReadDeadline(t time.Time) error { 79 | return c.orig.SetReadDeadline(t) 80 | } 81 | 82 | func (c *ObfsUDPConn) SetWriteDeadline(t time.Time) error { 83 | return c.orig.SetWriteDeadline(t) 84 | } 85 | 86 | func (c *ObfsUDPConn) SetReadBuffer(bytes int) error { 87 | return c.orig.SetReadBuffer(bytes) 88 | } 89 | 90 | func (c *ObfsUDPConn) SetWriteBuffer(bytes int) error { 91 | return c.orig.SetWriteBuffer(bytes) 92 | } 93 | 94 | func (c *ObfsUDPConn) SyscallConn() (syscall.RawConn, error) { 95 | return c.orig.SyscallConn() 96 | } 97 | 98 | func (c *ObfsUDPConn) File() (f *os.File, err error) { 99 | return c.orig.File() 100 | } 101 | -------------------------------------------------------------------------------- /pkg/redirect/tcp_linux.go: -------------------------------------------------------------------------------- 1 | package redirect 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "net" 7 | "syscall" 8 | "time" 9 | 10 | "github.com/HyNetwork/hysteria/pkg/core" 11 | "github.com/HyNetwork/hysteria/pkg/utils" 12 | ) 13 | 14 | type TCPRedirect struct { 15 | HyClient *core.Client 16 | ListenAddr *net.TCPAddr 17 | Timeout time.Duration 18 | 19 | ConnFunc func(addr, reqAddr net.Addr) 20 | ErrorFunc func(addr, reqAddr net.Addr, err error) 21 | } 22 | 23 | func NewTCPRedirect(hyClient *core.Client, listen string, timeout time.Duration, 24 | connFunc func(addr, reqAddr net.Addr), 25 | errorFunc func(addr, reqAddr net.Addr, err error), 26 | ) (*TCPRedirect, error) { 27 | tAddr, err := net.ResolveTCPAddr("tcp", listen) 28 | if err != nil { 29 | return nil, err 30 | } 31 | r := &TCPRedirect{ 32 | HyClient: hyClient, 33 | ListenAddr: tAddr, 34 | Timeout: timeout, 35 | ConnFunc: connFunc, 36 | ErrorFunc: errorFunc, 37 | } 38 | return r, nil 39 | } 40 | 41 | func (r *TCPRedirect) ListenAndServe() error { 42 | listener, err := net.ListenTCP("tcp", r.ListenAddr) 43 | if err != nil { 44 | return err 45 | } 46 | defer listener.Close() 47 | for { 48 | c, err := listener.Accept() 49 | if err != nil { 50 | return err 51 | } 52 | go func() { 53 | defer c.Close() 54 | dest, err := getDestAddr(c.(*net.TCPConn)) 55 | if err != nil || dest.IP.IsLoopback() { 56 | // Silently drop the connection if we failed to get the destination address, 57 | // or if it's a loopback address (not a redirected connection). 58 | return 59 | } 60 | r.ConnFunc(c.RemoteAddr(), dest) 61 | rc, err := r.HyClient.DialTCP(dest.String()) 62 | if err != nil { 63 | r.ErrorFunc(c.RemoteAddr(), dest, err) 64 | return 65 | } 66 | defer rc.Close() 67 | err = utils.PipePairWithTimeout(c, rc, r.Timeout) 68 | r.ErrorFunc(c.RemoteAddr(), dest, err) 69 | }() 70 | } 71 | } 72 | 73 | func getDestAddr(conn *net.TCPConn) (*net.TCPAddr, error) { 74 | rc, err := conn.SyscallConn() 75 | if err != nil { 76 | return nil, err 77 | } 78 | var addr *sockAddr 79 | var err2 error 80 | err = rc.Control(func(fd uintptr) { 81 | addr, err2 = getOrigDst(fd) 82 | }) 83 | if err != nil { 84 | return nil, err 85 | } 86 | if err2 != nil { 87 | return nil, err2 88 | } 89 | switch addr.family { 90 | case syscall.AF_INET: 91 | return &net.TCPAddr{IP: addr.data[:4], Port: int(binary.BigEndian.Uint16(addr.port[:]))}, nil 92 | case syscall.AF_INET6: 93 | return &net.TCPAddr{IP: addr.data[4:20], Port: int(binary.BigEndian.Uint16(addr.port[:]))}, nil 94 | default: 95 | return nil, errors.New("unknown address family") 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /pkg/tun/udp.go: -------------------------------------------------------------------------------- 1 | //go:build gpl 2 | // +build gpl 3 | 4 | package tun 5 | 6 | import ( 7 | "fmt" 8 | "net" 9 | "strconv" 10 | "time" 11 | 12 | "github.com/HyNetwork/hysteria/pkg/core" 13 | "github.com/xjasonlyu/tun2socks/v2/core/adapter" 14 | ) 15 | 16 | const udpBufferSize = 65535 17 | 18 | func (s *Server) HandleUDP(conn adapter.UDPConn) { 19 | go s.handleUDPConn(conn) 20 | } 21 | 22 | func (s *Server) handleUDPConn(conn adapter.UDPConn) { 23 | defer conn.Close() 24 | 25 | id := conn.ID() 26 | remoteAddr := net.UDPAddr{ 27 | IP: net.IP(id.LocalAddress), 28 | Port: int(id.LocalPort), 29 | } 30 | localAddr := net.UDPAddr{ 31 | IP: net.IP(id.RemoteAddress), 32 | Port: int(id.RemotePort), 33 | } 34 | 35 | if s.RequestFunc != nil { 36 | s.RequestFunc(&localAddr, remoteAddr.String()) 37 | } 38 | 39 | var err error 40 | defer func() { 41 | if s.ErrorFunc != nil && err != nil { 42 | s.ErrorFunc(&localAddr, remoteAddr.String(), err) 43 | } 44 | }() 45 | 46 | rc, err := s.HyClient.DialUDP() 47 | if err != nil { 48 | return 49 | } 50 | defer rc.Close() 51 | 52 | err = s.relayUDP(conn, rc, &remoteAddr, s.Timeout) 53 | } 54 | 55 | func (s *Server) relayUDP(lc adapter.UDPConn, rc core.UDPConn, to *net.UDPAddr, timeout time.Duration) (err error) { 56 | errChan := make(chan error, 2) 57 | // local => remote 58 | go func() { 59 | buf := make([]byte, udpBufferSize) 60 | for { 61 | if timeout != 0 { 62 | _ = lc.SetDeadline(time.Now().Add(timeout)) 63 | n, err := lc.Read(buf) 64 | if n > 0 { 65 | err = rc.WriteTo(buf[:n], to.String()) 66 | if err != nil { 67 | errChan <- err 68 | return 69 | } 70 | } 71 | if err != nil { 72 | errChan <- err 73 | return 74 | } 75 | } 76 | } 77 | }() 78 | // remote => local 79 | go func() { 80 | for { 81 | pkt, addr, err := rc.ReadFrom() 82 | if err != nil { 83 | errChan <- err 84 | return 85 | } 86 | if pkt != nil { 87 | host, portStr, err := net.SplitHostPort(addr) 88 | if err != nil { 89 | errChan <- err 90 | return 91 | } 92 | port, err := strconv.Atoi(portStr) 93 | if err != nil { 94 | errChan <- fmt.Errorf("cannot parse as port: %s", portStr) 95 | return 96 | } 97 | 98 | // adapter.UDPConn doesn't support WriteFrom() yet, 99 | // so we check the src address and behavior like a symmetric NAT 100 | if !to.IP.Equal(net.ParseIP(host)) || to.Port != port { 101 | // drop the packet silently 102 | continue 103 | } 104 | 105 | _, err = lc.Write(pkt) 106 | if err != nil { 107 | errChan <- err 108 | return 109 | } 110 | } 111 | } 112 | }() 113 | return <-errChan 114 | } 115 | -------------------------------------------------------------------------------- /pkg/http/server.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/HyNetwork/hysteria/pkg/transport" 11 | "github.com/HyNetwork/hysteria/pkg/utils" 12 | 13 | "github.com/elazarl/goproxy/ext/auth" 14 | 15 | "github.com/HyNetwork/hysteria/pkg/acl" 16 | "github.com/HyNetwork/hysteria/pkg/core" 17 | "github.com/elazarl/goproxy" 18 | ) 19 | 20 | func NewProxyHTTPServer(hyClient *core.Client, transport *transport.ClientTransport, idleTimeout time.Duration, 21 | aclEngine *acl.Engine, 22 | basicAuthFunc func(user, password string) bool, 23 | newDialFunc func(reqAddr string, action acl.Action, arg string), 24 | proxyErrorFunc func(reqAddr string, err error), 25 | ) (*goproxy.ProxyHttpServer, error) { 26 | proxy := goproxy.NewProxyHttpServer() 27 | proxy.Logger = &nopLogger{} 28 | proxy.NonproxyHandler = http.NotFoundHandler() 29 | proxy.Tr = &http.Transport{ 30 | Dial: func(network, addr string) (conn net.Conn, err error) { 31 | defer func() { 32 | if err != nil { 33 | proxyErrorFunc(addr, err) 34 | } 35 | }() 36 | // Parse addr string 37 | host, port, err := utils.SplitHostPort(addr) 38 | if err != nil { 39 | return nil, err 40 | } 41 | // ACL 42 | action, arg := acl.ActionProxy, "" 43 | var ipAddr *net.IPAddr 44 | var resErr error 45 | if aclEngine != nil { 46 | action, arg, _, ipAddr, resErr = aclEngine.ResolveAndMatch(host, port, false) 47 | // Doesn't always matter if the resolution fails, as we may send it through HyClient 48 | } 49 | newDialFunc(addr, action, arg) 50 | // Handle according to the action 51 | switch action { 52 | case acl.ActionDirect: 53 | if resErr != nil { 54 | return nil, resErr 55 | } 56 | return transport.DialTCP(&net.TCPAddr{ 57 | IP: ipAddr.IP, 58 | Port: int(port), 59 | Zone: ipAddr.Zone, 60 | }) 61 | case acl.ActionProxy: 62 | return hyClient.DialTCP(addr) 63 | case acl.ActionBlock: 64 | return nil, errors.New("blocked by ACL") 65 | case acl.ActionHijack: 66 | hijackIPAddr, err := transport.ResolveIPAddr(arg) 67 | if err != nil { 68 | return nil, err 69 | } 70 | return transport.DialTCP(&net.TCPAddr{ 71 | IP: hijackIPAddr.IP, 72 | Port: int(port), 73 | Zone: hijackIPAddr.Zone, 74 | }) 75 | default: 76 | return nil, fmt.Errorf("unknown action %d", action) 77 | } 78 | }, 79 | IdleConnTimeout: idleTimeout, 80 | // Disable HTTP2 support? ref: https://github.com/elazarl/goproxy/issues/361 81 | } 82 | proxy.ConnectDial = nil 83 | if basicAuthFunc != nil { 84 | auth.ProxyBasic(proxy, "hysteria client", basicAuthFunc) 85 | } 86 | return proxy, nil 87 | } 88 | 89 | type nopLogger struct{} 90 | 91 | func (n *nopLogger) Printf(format string, v ...interface{}) {} 92 | -------------------------------------------------------------------------------- /pkg/transport/client.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "net" 7 | "time" 8 | 9 | "github.com/HyNetwork/hysteria/pkg/conns/faketcp" 10 | "github.com/HyNetwork/hysteria/pkg/conns/udp" 11 | "github.com/HyNetwork/hysteria/pkg/conns/wechat" 12 | obfsPkg "github.com/HyNetwork/hysteria/pkg/obfs" 13 | "github.com/lucas-clemente/quic-go" 14 | ) 15 | 16 | type ClientTransport struct { 17 | Dialer *net.Dialer 18 | ResolvePreference ResolvePreference 19 | } 20 | 21 | var DefaultClientTransport = &ClientTransport{ 22 | Dialer: &net.Dialer{ 23 | Timeout: 8 * time.Second, 24 | }, 25 | ResolvePreference: ResolvePreferenceDefault, 26 | } 27 | 28 | func (ct *ClientTransport) quicPacketConn(proto string, server string, obfs obfsPkg.Obfuscator) (net.PacketConn, error) { 29 | if len(proto) == 0 || proto == "udp" { 30 | conn, err := net.ListenUDP("udp", nil) 31 | if err != nil { 32 | return nil, err 33 | } 34 | if obfs != nil { 35 | oc := udp.NewObfsUDPConn(conn, obfs) 36 | return oc, nil 37 | } else { 38 | return conn, nil 39 | } 40 | } else if proto == "wechat-video" { 41 | conn, err := net.ListenUDP("udp", nil) 42 | if err != nil { 43 | return nil, err 44 | } 45 | if obfs == nil { 46 | obfs = obfsPkg.NewDummyObfuscator() 47 | } 48 | return wechat.NewObfsWeChatUDPConn(conn, obfs), nil 49 | } else if proto == "faketcp" { 50 | var conn *faketcp.TCPConn 51 | conn, err := faketcp.Dial("tcp", server) 52 | if err != nil { 53 | return nil, err 54 | } 55 | if obfs != nil { 56 | oc := faketcp.NewObfsFakeTCPConn(conn, obfs) 57 | return oc, nil 58 | } else { 59 | return conn, nil 60 | } 61 | } else { 62 | return nil, fmt.Errorf("unsupported protocol: %s", proto) 63 | } 64 | } 65 | 66 | func (ct *ClientTransport) QUICDial(proto string, server string, tlsConfig *tls.Config, quicConfig *quic.Config, obfs obfsPkg.Obfuscator) (quic.Connection, error) { 67 | serverUDPAddr, err := net.ResolveUDPAddr("udp", server) 68 | if err != nil { 69 | return nil, err 70 | } 71 | pktConn, err := ct.quicPacketConn(proto, server, obfs) 72 | if err != nil { 73 | return nil, err 74 | } 75 | qs, err := quic.Dial(pktConn, serverUDPAddr, server, tlsConfig, quicConfig) 76 | if err != nil { 77 | _ = pktConn.Close() 78 | return nil, err 79 | } 80 | return qs, nil 81 | } 82 | 83 | func (ct *ClientTransport) ResolveIPAddr(address string) (*net.IPAddr, error) { 84 | return resolveIPAddrWithPreference(address, ct.ResolvePreference) 85 | } 86 | 87 | func (ct *ClientTransport) DialTCP(raddr *net.TCPAddr) (*net.TCPConn, error) { 88 | conn, err := ct.Dialer.Dial("tcp", raddr.String()) 89 | if err != nil { 90 | return nil, err 91 | } 92 | return conn.(*net.TCPConn), nil 93 | } 94 | 95 | func (ct *ClientTransport) ListenUDP() (*net.UDPConn, error) { 96 | return net.ListenUDP("udp", nil) 97 | } 98 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.2.1 4 | 5 | - Fix a bug that caused DNS failure when using domain names in the "resolver" option 6 | - Fix a bug where errors in HTTP proxy mode were not logged 7 | - Fix a bug where WeChat protocol was not working properly when obfuscation was not enabled 8 | - New TCP buffer options for tun mode (`tcp_sndbuf`, `tcp_rcvbuf`, `tcp_autotuning`) 9 | 10 | ## 1.2.0 11 | 12 | - Reworked TUN mode 13 | - DoT/DoH/DoQ support for resolver 14 | - IP masking (anonymization) 15 | - FreeBSD builds 16 | 17 | ## 1.1.0 18 | 19 | - Super major CPU performance improvements (~30% to several times faster, depending on the circumstances) by optimizing several data structures in quic-go (changes upstreamed) 20 | 21 | ## 1.0.5 22 | 23 | - `bind_outbound` server option for binding outbound connections to a specific address or interface 24 | - TCP Redirect mode (for Linux) 25 | 26 | ## 1.0.4 27 | 28 | - ~10% CPU usage reduction 29 | - Improve performance when packet loss is high 30 | - New ACL syntax to support protocol/port 31 | 32 | ## 1.0.3 33 | 34 | - New string-based speed (up/down) options 35 | - Server SOCKS5 outbound domain pass-through 36 | - Linux s390x build 37 | - Updated quic-go to v0.27.0 38 | 39 | ## 1.0.2 40 | 41 | - Added an option for DNS resolution preference `resolve_preference` 42 | 43 | ## 1.0.1 44 | 45 | - Fix server SOCKS5 outbound bug 46 | - Fix incorrect UDP fragmentation handling 47 | 48 | ## 1.0.0 49 | 50 | - Protocol v3: UDP fragmentation support 51 | - Fix SOCKS5 UDP timeout issue 52 | - SOCKS5 outbound support 53 | 54 | ## 0.9.7 55 | 56 | - CLI improvements (cobra) 57 | - Fix broken UDP TProxy mode 58 | - Re-enable PMTUD on Windows & Linux 59 | 60 | ## 0.9.6 61 | 62 | - Disable quic-go PMTUD due to broken implementation 63 | - Fix zero initMaxDatagramSize in brutal CC 64 | - Client retry 65 | 66 | ## 0.9.5 67 | 68 | - Client connect & disconnect log 69 | - Warning when no auth or obfs is set 70 | - Multi-password & cmd auth support 71 | 72 | ## 0.9.4 73 | 74 | - fsnotify-based auto keypair reloading 75 | - ACL country code support 76 | 77 | ## 0.9.3 78 | 79 | - CC optimizations 80 | - Set buffer correctly for faketcp mode 81 | - "wechat-video" protocol 82 | 83 | ## 0.9.2 84 | 85 | - Updated quic-go to v0.24.0 86 | - Reduced obfs overhead by reusing buffers 87 | 88 | ## 0.9.1 89 | 90 | - faketcp implementation 91 | - DNS `resolver` option in config 92 | 93 | ## 0.9.0 94 | 95 | - Auto keypair reloading 96 | - SOCKS5 listen address no longer needs a specific IP 97 | - Multi-relay support 98 | - IPv6 only mode for server 99 | 100 | ## 0.8.6 101 | 102 | - Added an option for customizing ALPN `alpn` 103 | - Removed ACL support from TPROXY & TUN modes 104 | 105 | ## 0.8.5 106 | 107 | - Added an option to disable MTU discovery `disable_mtu_discovery` 108 | -------------------------------------------------------------------------------- /pkg/conns/wechat/obfs.go: -------------------------------------------------------------------------------- 1 | package wechat 2 | 3 | import ( 4 | "encoding/binary" 5 | "math/rand" 6 | "net" 7 | "os" 8 | "sync" 9 | "syscall" 10 | "time" 11 | 12 | "github.com/HyNetwork/hysteria/pkg/obfs" 13 | ) 14 | 15 | const udpBufferSize = 65535 16 | 17 | type ObfsWeChatUDPConn struct { 18 | orig *net.UDPConn 19 | obfs obfs.Obfuscator 20 | 21 | readBuf []byte 22 | readMutex sync.Mutex 23 | writeBuf []byte 24 | writeMutex sync.Mutex 25 | sn uint32 26 | } 27 | 28 | func NewObfsWeChatUDPConn(orig *net.UDPConn, obfs obfs.Obfuscator) *ObfsWeChatUDPConn { 29 | return &ObfsWeChatUDPConn{ 30 | orig: orig, 31 | obfs: obfs, 32 | readBuf: make([]byte, udpBufferSize), 33 | writeBuf: make([]byte, udpBufferSize), 34 | sn: rand.Uint32() & 0xFFFF, 35 | } 36 | } 37 | 38 | func (c *ObfsWeChatUDPConn) ReadFrom(p []byte) (int, net.Addr, error) { 39 | for { 40 | c.readMutex.Lock() 41 | n, addr, err := c.orig.ReadFrom(c.readBuf) 42 | if n <= 13 { 43 | c.readMutex.Unlock() 44 | return 0, addr, err 45 | } 46 | newN := c.obfs.Deobfuscate(c.readBuf[13:n], p) 47 | c.readMutex.Unlock() 48 | if newN > 0 { 49 | // Valid packet 50 | return newN, addr, err 51 | } else if err != nil { 52 | // Not valid and orig.ReadFrom had some error 53 | return 0, addr, err 54 | } 55 | } 56 | } 57 | 58 | func (c *ObfsWeChatUDPConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { 59 | c.writeMutex.Lock() 60 | c.writeBuf[0] = 0xa1 61 | c.writeBuf[1] = 0x08 62 | binary.BigEndian.PutUint32(c.writeBuf[2:], c.sn) 63 | c.sn++ 64 | c.writeBuf[6] = 0x00 65 | c.writeBuf[7] = 0x10 66 | c.writeBuf[8] = 0x11 67 | c.writeBuf[9] = 0x18 68 | c.writeBuf[10] = 0x30 69 | c.writeBuf[11] = 0x22 70 | c.writeBuf[12] = 0x30 71 | bn := c.obfs.Obfuscate(p, c.writeBuf[13:]) 72 | _, err = c.orig.WriteTo(c.writeBuf[:13+bn], addr) 73 | c.writeMutex.Unlock() 74 | if err != nil { 75 | return 0, err 76 | } else { 77 | return len(p), nil 78 | } 79 | } 80 | 81 | func (c *ObfsWeChatUDPConn) Close() error { 82 | return c.orig.Close() 83 | } 84 | 85 | func (c *ObfsWeChatUDPConn) LocalAddr() net.Addr { 86 | return c.orig.LocalAddr() 87 | } 88 | 89 | func (c *ObfsWeChatUDPConn) SetDeadline(t time.Time) error { 90 | return c.orig.SetDeadline(t) 91 | } 92 | 93 | func (c *ObfsWeChatUDPConn) SetReadDeadline(t time.Time) error { 94 | return c.orig.SetReadDeadline(t) 95 | } 96 | 97 | func (c *ObfsWeChatUDPConn) SetWriteDeadline(t time.Time) error { 98 | return c.orig.SetWriteDeadline(t) 99 | } 100 | 101 | func (c *ObfsWeChatUDPConn) SetReadBuffer(bytes int) error { 102 | return c.orig.SetReadBuffer(bytes) 103 | } 104 | 105 | func (c *ObfsWeChatUDPConn) SetWriteBuffer(bytes int) error { 106 | return c.orig.SetWriteBuffer(bytes) 107 | } 108 | 109 | func (c *ObfsWeChatUDPConn) SyscallConn() (syscall.RawConn, error) { 110 | return c.orig.SyscallConn() 111 | } 112 | 113 | func (c *ObfsWeChatUDPConn) File() (f *os.File, err error) { 114 | return c.orig.File() 115 | } 116 | -------------------------------------------------------------------------------- /pkg/tproxy/udp_linux.go: -------------------------------------------------------------------------------- 1 | package tproxy 2 | 3 | import ( 4 | "net" 5 | "time" 6 | 7 | "github.com/HyNetwork/hysteria/pkg/core" 8 | "github.com/LiamHaworth/go-tproxy" 9 | ) 10 | 11 | const udpBufferSize = 65535 12 | 13 | type UDPTProxy struct { 14 | HyClient *core.Client 15 | ListenAddr *net.UDPAddr 16 | Timeout time.Duration 17 | 18 | ConnFunc func(addr, reqAddr net.Addr) 19 | ErrorFunc func(addr, reqAddr net.Addr, err error) 20 | } 21 | 22 | func NewUDPTProxy(hyClient *core.Client, listen string, timeout time.Duration, 23 | connFunc func(addr, reqAddr net.Addr), 24 | errorFunc func(addr, reqAddr net.Addr, err error), 25 | ) (*UDPTProxy, error) { 26 | uAddr, err := net.ResolveUDPAddr("udp", listen) 27 | if err != nil { 28 | return nil, err 29 | } 30 | r := &UDPTProxy{ 31 | HyClient: hyClient, 32 | ListenAddr: uAddr, 33 | Timeout: timeout, 34 | ConnFunc: connFunc, 35 | ErrorFunc: errorFunc, 36 | } 37 | if timeout == 0 { 38 | r.Timeout = 1 * time.Minute 39 | } 40 | return r, nil 41 | } 42 | 43 | func (r *UDPTProxy) ListenAndServe() error { 44 | conn, err := tproxy.ListenUDP("udp", r.ListenAddr) 45 | if err != nil { 46 | return err 47 | } 48 | defer conn.Close() 49 | // Read loop 50 | buf := make([]byte, udpBufferSize) 51 | for { 52 | n, srcAddr, dstAddr, err := tproxy.ReadFromUDP(conn, buf) // Huge Caveat!! This essentially works as TCP's Accept here - won't repeat for the same srcAddr/dstAddr pair - because and only because we have tproxy.DialUDP("udp", dstAddr, srcAddr) to take over the connection below 53 | if n > 0 { 54 | r.ConnFunc(srcAddr, dstAddr) 55 | localConn, err := tproxy.DialUDP("udp", dstAddr, srcAddr) 56 | if err != nil { 57 | r.ErrorFunc(srcAddr, dstAddr, err) 58 | continue 59 | } 60 | hyConn, err := r.HyClient.DialUDP() 61 | if err != nil { 62 | r.ErrorFunc(srcAddr, dstAddr, err) 63 | _ = localConn.Close() 64 | continue 65 | } 66 | _ = hyConn.WriteTo(buf[:n], dstAddr.String()) 67 | 68 | errChan := make(chan error, 2) 69 | // Start remote to local 70 | go func() { 71 | for { 72 | bs, _, err := hyConn.ReadFrom() 73 | if err != nil { 74 | errChan <- err 75 | return 76 | } 77 | _, err = localConn.Write(bs) 78 | if err != nil { 79 | errChan <- err 80 | return 81 | } 82 | _ = localConn.SetDeadline(time.Now().Add(r.Timeout)) 83 | } 84 | }() 85 | // Start local to remote 86 | go func() { 87 | for { 88 | _ = localConn.SetDeadline(time.Now().Add(r.Timeout)) 89 | n, err := localConn.Read(buf) 90 | if n > 0 { 91 | err := hyConn.WriteTo(buf[:n], dstAddr.String()) 92 | if err != nil { 93 | errChan <- err 94 | return 95 | } 96 | } 97 | if err != nil { 98 | errChan <- err 99 | return 100 | } 101 | } 102 | }() 103 | // Error cleanup routine 104 | go func() { 105 | err := <-errChan 106 | _ = localConn.Close() 107 | _ = hyConn.Close() 108 | r.ErrorFunc(srcAddr, dstAddr, err) 109 | }() 110 | } 111 | if err != nil { 112 | return err 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /pkg/relay/udp.go: -------------------------------------------------------------------------------- 1 | package relay 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | "sync" 7 | "sync/atomic" 8 | "time" 9 | 10 | "github.com/HyNetwork/hysteria/pkg/core" 11 | ) 12 | 13 | const udpBufferSize = 65535 14 | 15 | var ErrTimeout = errors.New("inactivity timeout") 16 | 17 | type UDPRelay struct { 18 | HyClient *core.Client 19 | ListenAddr *net.UDPAddr 20 | Remote string 21 | Timeout time.Duration 22 | 23 | ConnFunc func(addr net.Addr) 24 | ErrorFunc func(addr net.Addr, err error) 25 | } 26 | 27 | func NewUDPRelay(hyClient *core.Client, listen, remote string, timeout time.Duration, 28 | connFunc func(addr net.Addr), errorFunc func(addr net.Addr, err error), 29 | ) (*UDPRelay, error) { 30 | uAddr, err := net.ResolveUDPAddr("udp", listen) 31 | if err != nil { 32 | return nil, err 33 | } 34 | r := &UDPRelay{ 35 | HyClient: hyClient, 36 | ListenAddr: uAddr, 37 | Remote: remote, 38 | Timeout: timeout, 39 | ConnFunc: connFunc, 40 | ErrorFunc: errorFunc, 41 | } 42 | if timeout == 0 { 43 | r.Timeout = 1 * time.Minute 44 | } 45 | return r, nil 46 | } 47 | 48 | type connEntry struct { 49 | HyConn core.UDPConn 50 | Deadline atomic.Value 51 | } 52 | 53 | func (r *UDPRelay) ListenAndServe() error { 54 | conn, err := net.ListenUDP("udp", r.ListenAddr) 55 | if err != nil { 56 | return err 57 | } 58 | defer conn.Close() 59 | // src <-> HyClient UDPConn 60 | connMap := make(map[string]*connEntry) 61 | var connMapMutex sync.RWMutex 62 | // Read loop 63 | buf := make([]byte, udpBufferSize) 64 | for { 65 | n, rAddr, err := conn.ReadFromUDP(buf) 66 | if n > 0 { 67 | connMapMutex.RLock() 68 | entry := connMap[rAddr.String()] 69 | connMapMutex.RUnlock() 70 | if entry != nil { 71 | // Existing conn 72 | entry.Deadline.Store(time.Now().Add(r.Timeout)) 73 | _ = entry.HyConn.WriteTo(buf[:n], r.Remote) 74 | } else { 75 | // New 76 | r.ConnFunc(rAddr) 77 | hyConn, err := r.HyClient.DialUDP() 78 | if err != nil { 79 | r.ErrorFunc(rAddr, err) 80 | } else { 81 | // Add it to the map 82 | entry := &connEntry{HyConn: hyConn} 83 | entry.Deadline.Store(time.Now().Add(r.Timeout)) 84 | connMapMutex.Lock() 85 | connMap[rAddr.String()] = entry 86 | connMapMutex.Unlock() 87 | // Start remote to local 88 | go func() { 89 | for { 90 | bs, _, err := hyConn.ReadFrom() 91 | if err != nil { 92 | break 93 | } 94 | entry.Deadline.Store(time.Now().Add(r.Timeout)) 95 | _, _ = conn.WriteToUDP(bs, rAddr) 96 | } 97 | }() 98 | // Timeout cleanup routine 99 | go func() { 100 | for { 101 | ttl := entry.Deadline.Load().(time.Time).Sub(time.Now()) 102 | if ttl <= 0 { 103 | // Time to die 104 | connMapMutex.Lock() 105 | _ = hyConn.Close() 106 | delete(connMap, rAddr.String()) 107 | connMapMutex.Unlock() 108 | r.ErrorFunc(rAddr, ErrTimeout) 109 | return 110 | } else { 111 | time.Sleep(ttl) 112 | } 113 | } 114 | }() 115 | // Send the packet 116 | _ = hyConn.WriteTo(buf[:n], r.Remote) 117 | } 118 | } 119 | } 120 | if err != nil { 121 | return err 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /cmd/resolver.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | "net/url" 7 | "strings" 8 | 9 | "github.com/HyNetwork/hysteria/pkg/utils" 10 | rdns "github.com/folbricht/routedns" 11 | ) 12 | 13 | var errInvalidSyntax = errors.New("invalid syntax") 14 | 15 | func setResolver(dns string) error { 16 | if net.ParseIP(dns) != nil { 17 | // Just an IP address, treat as UDP 53 18 | dns = "udp://" + net.JoinHostPort(dns, "53") 19 | } 20 | var r rdns.Resolver 21 | if strings.HasPrefix(dns, "udp://") { 22 | // Standard UDP DNS resolver 23 | dns = strings.TrimPrefix(dns, "udp://") 24 | if dns == "" { 25 | return errInvalidSyntax 26 | } 27 | if _, _, err := utils.SplitHostPort(dns); err != nil { 28 | // Append the default DNS port 29 | dns = net.JoinHostPort(dns, "53") 30 | } 31 | client, err := rdns.NewDNSClient("dns-udp", dns, "udp", rdns.DNSClientOptions{}) 32 | if err != nil { 33 | return err 34 | } 35 | r = client 36 | } else if strings.HasPrefix(dns, "tcp://") { 37 | // Standard TCP DNS resolver 38 | dns = strings.TrimPrefix(dns, "tcp://") 39 | if dns == "" { 40 | return errInvalidSyntax 41 | } 42 | if _, _, err := utils.SplitHostPort(dns); err != nil { 43 | // Append the default DNS port 44 | dns = net.JoinHostPort(dns, "53") 45 | } 46 | client, err := rdns.NewDNSClient("dns-tcp", dns, "tcp", rdns.DNSClientOptions{}) 47 | if err != nil { 48 | return err 49 | } 50 | r = client 51 | } else if strings.HasPrefix(dns, "https://") { 52 | // DoH resolver 53 | if dohURL, err := url.Parse(dns); err != nil { 54 | return err 55 | } else { 56 | // Need to set bootstrap address to avoid loopback DNS lookup 57 | dohIPAddr, err := net.ResolveIPAddr("ip", dohURL.Hostname()) 58 | if err != nil { 59 | return err 60 | } 61 | client, err := rdns.NewDoHClient("doh", dns, rdns.DoHClientOptions{ 62 | BootstrapAddr: dohIPAddr.String(), 63 | }) 64 | if err != nil { 65 | return err 66 | } 67 | r = client 68 | } 69 | } else if strings.HasPrefix(dns, "tls://") { 70 | // DoT resolver 71 | dns = strings.TrimPrefix(dns, "tls://") 72 | if dns == "" { 73 | return errInvalidSyntax 74 | } 75 | dotHost, _, err := utils.SplitHostPort(dns) 76 | if err != nil { 77 | // Append the default DNS port 78 | dns = net.JoinHostPort(dns, "853") 79 | } 80 | // Need to set bootstrap address to avoid loopback DNS lookup 81 | dotIPAddr, err := net.ResolveIPAddr("ip", dotHost) 82 | if err != nil { 83 | return err 84 | } 85 | client, err := rdns.NewDoTClient("dot", dns, rdns.DoTClientOptions{ 86 | BootstrapAddr: dotIPAddr.String(), 87 | }) 88 | if err != nil { 89 | return err 90 | } 91 | r = client 92 | } else if strings.HasPrefix(dns, "quic://") { 93 | // DoQ resolver 94 | dns = strings.TrimPrefix(dns, "quic://") 95 | if dns == "" { 96 | return errInvalidSyntax 97 | } 98 | doqHost, _, err := utils.SplitHostPort(dns) 99 | if err != nil { 100 | // Append the default DNS port 101 | dns = net.JoinHostPort(dns, "853") 102 | } 103 | // Need to set bootstrap address to avoid loopback DNS lookup 104 | doqIPAddr, err := net.ResolveIPAddr("ip", doqHost) 105 | if err != nil { 106 | return err 107 | } 108 | client, err := rdns.NewDoQClient("doq", dns, rdns.DoQClientOptions{ 109 | BootstrapAddr: doqIPAddr.String(), 110 | }) 111 | if err != nil { 112 | return err 113 | } 114 | r = client 115 | } else { 116 | return errInvalidSyntax 117 | } 118 | cache := rdns.NewCache("cache", r, rdns.CacheOptions{}) 119 | net.DefaultResolver = rdns.NewNetResolver(cache) 120 | return nil 121 | } 122 | -------------------------------------------------------------------------------- /pkg/acl/engine_test.go: -------------------------------------------------------------------------------- 1 | package acl 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | "strings" 7 | "testing" 8 | 9 | lru "github.com/hashicorp/golang-lru" 10 | ) 11 | 12 | func TestEngine_ResolveAndMatch(t *testing.T) { 13 | cache, _ := lru.NewARC(16) 14 | e := &Engine{ 15 | DefaultAction: ActionDirect, 16 | Entries: []Entry{ 17 | { 18 | Action: ActionProxy, 19 | ActionArg: "", 20 | Matcher: &domainMatcher{ 21 | matcherBase: matcherBase{ 22 | Protocol: ProtocolTCP, 23 | Port: 443, 24 | }, 25 | Domain: "google.com", 26 | Suffix: false, 27 | }, 28 | }, 29 | { 30 | Action: ActionHijack, 31 | ActionArg: "good.org", 32 | Matcher: &domainMatcher{ 33 | matcherBase: matcherBase{}, 34 | Domain: "evil.corp", 35 | Suffix: true, 36 | }, 37 | }, 38 | { 39 | Action: ActionProxy, 40 | ActionArg: "", 41 | Matcher: &netMatcher{ 42 | matcherBase: matcherBase{}, 43 | Net: &net.IPNet{ 44 | IP: net.ParseIP("10.0.0.0"), 45 | Mask: net.CIDRMask(8, 32), 46 | }, 47 | }, 48 | }, 49 | { 50 | Action: ActionBlock, 51 | ActionArg: "", 52 | Matcher: &allMatcher{}, 53 | }, 54 | }, 55 | Cache: cache, 56 | ResolveIPAddr: func(s string) (*net.IPAddr, error) { 57 | if strings.Contains(s, "evil.corp") { 58 | return nil, errors.New("resolve error") 59 | } 60 | return net.ResolveIPAddr("ip", s) 61 | }, 62 | } 63 | tests := []struct { 64 | name string 65 | host string 66 | port uint16 67 | isUDP bool 68 | wantAction Action 69 | wantArg string 70 | wantErr bool 71 | }{ 72 | { 73 | name: "domain proxy", 74 | host: "google.com", 75 | port: 443, 76 | isUDP: false, 77 | wantAction: ActionProxy, 78 | wantArg: "", 79 | }, 80 | { 81 | name: "domain block", 82 | host: "google.com", 83 | port: 80, 84 | isUDP: false, 85 | wantAction: ActionBlock, 86 | wantArg: "", 87 | }, 88 | { 89 | name: "domain suffix 1", 90 | host: "evil.corp", 91 | port: 8899, 92 | isUDP: true, 93 | wantAction: ActionHijack, 94 | wantArg: "good.org", 95 | wantErr: true, 96 | }, 97 | { 98 | name: "domain suffix 2", 99 | host: "notevil.corp", 100 | port: 22, 101 | isUDP: false, 102 | wantAction: ActionBlock, 103 | wantArg: "", 104 | wantErr: true, 105 | }, 106 | { 107 | name: "domain suffix 3", 108 | host: "im.real.evil.corp", 109 | port: 443, 110 | isUDP: true, 111 | wantAction: ActionHijack, 112 | wantArg: "good.org", 113 | wantErr: true, 114 | }, 115 | { 116 | name: "ip match", 117 | host: "10.2.3.4", 118 | port: 80, 119 | isUDP: false, 120 | wantAction: ActionProxy, 121 | wantArg: "", 122 | }, 123 | { 124 | name: "ip mismatch", 125 | host: "100.5.6.0", 126 | port: 1234, 127 | isUDP: false, 128 | wantAction: ActionBlock, 129 | wantArg: "", 130 | }, 131 | { 132 | name: "domain proxy cache", 133 | host: "google.com", 134 | port: 443, 135 | isUDP: false, 136 | wantAction: ActionProxy, 137 | wantArg: "", 138 | }, 139 | } 140 | for _, tt := range tests { 141 | t.Run(tt.name, func(t *testing.T) { 142 | gotAction, gotArg, _, _, err := e.ResolveAndMatch(tt.host, tt.port, tt.isUDP) 143 | if (err != nil) != tt.wantErr { 144 | t.Errorf("ResolveAndMatch() error = %v, wantErr %v", err, tt.wantErr) 145 | return 146 | } 147 | if gotAction != tt.wantAction { 148 | t.Errorf("ResolveAndMatch() gotAction = %v, wantAction %v", gotAction, tt.wantAction) 149 | } 150 | if gotArg != tt.wantArg { 151 | t.Errorf("ResolveAndMatch() gotArg = %v, wantAction %v", gotArg, tt.wantArg) 152 | } 153 | }) 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /Taskfile.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | tasks: 4 | clean: 5 | cmds: 6 | - rm -rf dist 7 | - mkdir -p dist 8 | build-hysteria: 9 | label: build-{{.TASK}} 10 | vars: 11 | BUILD_COMMIT: 12 | sh: git rev-parse HEAD 13 | BUILD_DATE: 14 | sh: date "+%Y%m%d%H%M%S" 15 | dir: ./cmd 16 | cmds: 17 | - | 18 | GOOS={{.GOOS}} GOARCH={{.GOARCH}} GOARM={{.GOARM}} GOMIPS={{.GOMIPS}} \ 19 | go build -trimpath -o ../dist/hysteria-{{.TASK}} -ldflags \ 20 | "-w -s -X main.appCommit={{.BUILD_COMMIT}} -X main.appDate={{.BUILD_DATE}}" 21 | linux-386: 22 | cmds: 23 | - task: build-hysteria 24 | vars: { 25 | TASK: "{{.TASK}}", 26 | GOOS: linux, 27 | GOARCH: 386 28 | } 29 | linux-amd64: 30 | cmds: 31 | - task: build-hysteria 32 | vars: { 33 | TASK: "{{.TASK}}", 34 | GOOS: linux, 35 | GOARCH: amd64 36 | } 37 | linux-amd64-v3: 38 | cmds: 39 | - task: build-hysteria 40 | vars: { 41 | TASK: "{{.TASK}}", 42 | GOOS: linux, 43 | GOARCH: amd64, 44 | GOAMD64: v3 45 | } 46 | linux-armv5: 47 | cmds: 48 | - task: build-hysteria 49 | vars: { 50 | TASK: "{{.TASK}}", 51 | GOOS: linux, 52 | GOARCH: arm, 53 | GOARM: 5 54 | } 55 | linux-armv6: 56 | cmds: 57 | - task: build-hysteria 58 | vars: { 59 | TASK: "{{.TASK}}", 60 | GOOS: linux, 61 | GOARCH: arm, 62 | GOARM: 6 63 | } 64 | linux-armv7: 65 | cmds: 66 | - task: build-hysteria 67 | vars: { 68 | TASK: "{{.TASK}}", 69 | GOOS: linux, 70 | GOARCH: arm, 71 | GOARM: 7 72 | } 73 | linux-armv8: 74 | cmds: 75 | - task: build-hysteria 76 | vars: { 77 | TASK: "{{.TASK}}", 78 | GOOS: linux, 79 | GOARCH: arm64 80 | } 81 | linux-mips-hardfloat: 82 | cmds: 83 | - task: build-hysteria 84 | vars: { 85 | TASK: "{{.TASK}}", 86 | GOOS: linux, 87 | GOARCH: mips, 88 | GOMIPS: hardfloat 89 | } 90 | linux-mipsle-softfloat: 91 | cmds: 92 | - task: build-hysteria 93 | vars: { 94 | TASK: "{{.TASK}}", 95 | GOOS: linux, 96 | GOARCH: mipsle, 97 | GOMIPS: softfloat 98 | } 99 | linux-mipsle-hardfloat: 100 | cmds: 101 | - task: build-hysteria 102 | vars: { 103 | TASK: "{{.TASK}}", 104 | GOOS: linux, 105 | GOARCH: mipsle, 106 | GOMIPS: hardfloat 107 | } 108 | linux-mips64: 109 | cmds: 110 | - task: build-hysteria 111 | vars: { 112 | TASK: "{{.TASK}}", 113 | GOOS: linux, 114 | GOARCH: mips64 115 | } 116 | linux-mips64le: 117 | cmds: 118 | - task: build-hysteria 119 | vars: { 120 | TASK: "{{.TASK}}", 121 | GOOS: linux, 122 | GOARCH: mips64le 123 | } 124 | darwin-amd64: 125 | cmds: 126 | - task: build-hysteria 127 | vars: { 128 | TASK: "{{.TASK}}", 129 | GOOS: darwin, 130 | GOARCH: amd64 131 | } 132 | darwin-arm64: 133 | cmds: 134 | - task: build-hysteria 135 | vars: { 136 | TASK: "{{.TASK}}", 137 | GOOS: darwin, 138 | GOARCH: arm64 139 | } 140 | default: 141 | cmds: 142 | - task: clean 143 | - task: linux-386 144 | - task: linux-amd64 145 | - task: linux-amd64-v3 146 | - task: linux-armv5 147 | - task: linux-armv6 148 | - task: linux-armv7 149 | - task: linux-armv8 150 | - task: linux-mips-hardfloat 151 | - task: linux-mipsle-softfloat 152 | - task: linux-mipsle-hardfloat 153 | - task: linux-mips64 154 | - task: linux-mips64le 155 | - task: darwin-amd64 156 | - task: darwin-arm64 157 | -------------------------------------------------------------------------------- /pkg/acl/engine.go: -------------------------------------------------------------------------------- 1 | package acl 2 | 3 | import ( 4 | "bufio" 5 | "net" 6 | "os" 7 | "strings" 8 | 9 | "github.com/HyNetwork/hysteria/pkg/utils" 10 | lru "github.com/hashicorp/golang-lru" 11 | "github.com/oschwald/geoip2-golang" 12 | ) 13 | 14 | const entryCacheSize = 1024 15 | 16 | type Engine struct { 17 | DefaultAction Action 18 | Entries []Entry 19 | Cache *lru.ARCCache 20 | ResolveIPAddr func(string) (*net.IPAddr, error) 21 | GeoIPReader *geoip2.Reader 22 | } 23 | 24 | type cacheKey struct { 25 | Host string 26 | Port uint16 27 | IsUDP bool 28 | } 29 | 30 | type cacheValue struct { 31 | Action Action 32 | Arg string 33 | } 34 | 35 | func LoadFromFile(filename string, resolveIPAddr func(string) (*net.IPAddr, error), geoIPLoadFunc func() (*geoip2.Reader, error)) (*Engine, error) { 36 | f, err := os.Open(filename) 37 | if err != nil { 38 | return nil, err 39 | } 40 | defer f.Close() 41 | scanner := bufio.NewScanner(f) 42 | entries := make([]Entry, 0, 1024) 43 | var geoIPReader *geoip2.Reader 44 | for scanner.Scan() { 45 | line := strings.TrimSpace(scanner.Text()) 46 | if len(line) == 0 || strings.HasPrefix(line, "#") { 47 | // Ignore empty lines & comments 48 | continue 49 | } 50 | entry, err := ParseEntry(line) 51 | if err != nil { 52 | return nil, err 53 | } 54 | if _, ok := entry.Matcher.(*countryMatcher); ok && geoIPReader == nil { 55 | geoIPReader, err = geoIPLoadFunc() // lazy load GeoIP reader only when needed 56 | if err != nil { 57 | return nil, err 58 | } 59 | } 60 | entries = append(entries, entry) 61 | } 62 | cache, err := lru.NewARC(entryCacheSize) 63 | if err != nil { 64 | return nil, err 65 | } 66 | return &Engine{ 67 | DefaultAction: ActionProxy, 68 | Entries: entries, 69 | Cache: cache, 70 | ResolveIPAddr: resolveIPAddr, 71 | GeoIPReader: geoIPReader, 72 | }, nil 73 | } 74 | 75 | // action, arg, isDomain, resolvedIP, error 76 | func (e *Engine) ResolveAndMatch(host string, port uint16, isUDP bool) (Action, string, bool, *net.IPAddr, error) { 77 | ip, zone := utils.ParseIPZone(host) 78 | if ip == nil { 79 | // Domain 80 | ipAddr, err := e.ResolveIPAddr(host) 81 | if v, ok := e.Cache.Get(cacheKey{host, port, isUDP}); ok { 82 | // Cache hit 83 | ce := v.(cacheValue) 84 | return ce.Action, ce.Arg, true, ipAddr, err 85 | } 86 | for _, entry := range e.Entries { 87 | mReq := MatchRequest{ 88 | Domain: host, 89 | Port: port, 90 | DB: e.GeoIPReader, 91 | } 92 | if ipAddr != nil { 93 | mReq.IP = ipAddr.IP 94 | } 95 | if isUDP { 96 | mReq.Protocol = ProtocolUDP 97 | } else { 98 | mReq.Protocol = ProtocolTCP 99 | } 100 | if entry.Match(mReq) { 101 | e.Cache.Add(cacheKey{host, port, isUDP}, 102 | cacheValue{entry.Action, entry.ActionArg}) 103 | return entry.Action, entry.ActionArg, true, ipAddr, err 104 | } 105 | } 106 | e.Cache.Add(cacheKey{host, port, isUDP}, cacheValue{e.DefaultAction, ""}) 107 | return e.DefaultAction, "", true, ipAddr, err 108 | } else { 109 | // IP 110 | if v, ok := e.Cache.Get(cacheKey{ip.String(), port, isUDP}); ok { 111 | // Cache hit 112 | ce := v.(cacheValue) 113 | return ce.Action, ce.Arg, false, &net.IPAddr{ 114 | IP: ip, 115 | Zone: zone, 116 | }, nil 117 | } 118 | for _, entry := range e.Entries { 119 | mReq := MatchRequest{ 120 | IP: ip, 121 | Port: port, 122 | DB: e.GeoIPReader, 123 | } 124 | if isUDP { 125 | mReq.Protocol = ProtocolUDP 126 | } else { 127 | mReq.Protocol = ProtocolTCP 128 | } 129 | if entry.Match(mReq) { 130 | e.Cache.Add(cacheKey{ip.String(), port, isUDP}, 131 | cacheValue{entry.Action, entry.ActionArg}) 132 | return entry.Action, entry.ActionArg, false, &net.IPAddr{ 133 | IP: ip, 134 | Zone: zone, 135 | }, nil 136 | } 137 | } 138 | e.Cache.Add(cacheKey{ip.String(), port, isUDP}, cacheValue{e.DefaultAction, ""}) 139 | return e.DefaultAction, "", false, &net.IPAddr{ 140 | IP: ip, 141 | Zone: zone, 142 | }, nil 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/go,linux,macos,windows,intellij+all 2 | # Edit at https://www.gitignore.io/?templates=go,linux,macos,windows,intellij+all 3 | 4 | ### Go ### 5 | # Binaries for programs and plugins 6 | *.exe 7 | *.exe~ 8 | *.dll 9 | *.so 10 | *.dylib 11 | 12 | # Test binary, built with `go test -c` 13 | *.test 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | 18 | # Dependency directories (remove the comment below to include it) 19 | # vendor/ 20 | 21 | ### Go Patch ### 22 | /vendor/ 23 | /Godeps/ 24 | 25 | ### Intellij+all ### 26 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 27 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 28 | 29 | # User-specific stuff 30 | .idea/**/workspace.xml 31 | .idea/**/tasks.xml 32 | .idea/**/usage.statistics.xml 33 | .idea/**/dictionaries 34 | .idea/**/shelf 35 | 36 | # Generated files 37 | .idea/**/contentModel.xml 38 | 39 | # Sensitive or high-churn files 40 | .idea/**/dataSources/ 41 | .idea/**/dataSources.ids 42 | .idea/**/dataSources.local.xml 43 | .idea/**/sqlDataSources.xml 44 | .idea/**/dynamic.xml 45 | .idea/**/uiDesigner.xml 46 | .idea/**/dbnavigator.xml 47 | 48 | # Gradle 49 | .idea/**/gradle.xml 50 | .idea/**/libraries 51 | 52 | # Gradle and Maven with auto-import 53 | # When using Gradle or Maven with auto-import, you should exclude module files, 54 | # since they will be recreated, and may cause churn. Uncomment if using 55 | # auto-import. 56 | # .idea/modules.xml 57 | # .idea/*.iml 58 | # .idea/modules 59 | # *.iml 60 | # *.ipr 61 | 62 | # CMake 63 | cmake-build-*/ 64 | 65 | # Mongo Explorer plugin 66 | .idea/**/mongoSettings.xml 67 | 68 | # File-based project format 69 | *.iws 70 | 71 | # IntelliJ 72 | out/ 73 | 74 | # mpeltonen/sbt-idea plugin 75 | .idea_modules/ 76 | 77 | # JIRA plugin 78 | atlassian-ide-plugin.xml 79 | 80 | # Cursive Clojure plugin 81 | .idea/replstate.xml 82 | 83 | # Crashlytics plugin (for Android Studio and IntelliJ) 84 | com_crashlytics_export_strings.xml 85 | crashlytics.properties 86 | crashlytics-build.properties 87 | fabric.properties 88 | 89 | # Editor-based Rest Client 90 | .idea/httpRequests 91 | 92 | # Android studio 3.1+ serialized cache file 93 | .idea/caches/build_file_checksums.ser 94 | 95 | ### Intellij+all Patch ### 96 | # Ignores the whole .idea folder and all .iml files 97 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 98 | 99 | .idea/ 100 | 101 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 102 | 103 | *.iml 104 | modules.xml 105 | .idea/misc.xml 106 | *.ipr 107 | 108 | # Sonarlint plugin 109 | .idea/sonarlint 110 | 111 | ### Linux ### 112 | *~ 113 | 114 | # temporary files which can be created if a process still has a handle open of a deleted file 115 | .fuse_hidden* 116 | 117 | # KDE directory preferences 118 | .directory 119 | 120 | # Linux trash folder which might appear on any partition or disk 121 | .Trash-* 122 | 123 | # .nfs files are created when an open file is removed but is still being accessed 124 | .nfs* 125 | 126 | ### macOS ### 127 | # General 128 | .DS_Store 129 | .AppleDouble 130 | .LSOverride 131 | 132 | # Icon must end with two \r 133 | Icon 134 | 135 | # Thumbnails 136 | ._* 137 | 138 | # Files that might appear in the root of a volume 139 | .DocumentRevisions-V100 140 | .fseventsd 141 | .Spotlight-V100 142 | .TemporaryItems 143 | .Trashes 144 | .VolumeIcon.icns 145 | .com.apple.timemachine.donotpresent 146 | 147 | # Directories potentially created on remote AFP share 148 | .AppleDB 149 | .AppleDesktop 150 | Network Trash Folder 151 | Temporary Items 152 | .apdisk 153 | 154 | ### Windows ### 155 | # Windows thumbnail cache files 156 | Thumbs.db 157 | Thumbs.db:encryptable 158 | ehthumbs.db 159 | ehthumbs_vista.db 160 | 161 | # Dump file 162 | *.stackdump 163 | 164 | # Folder config file 165 | [Dd]esktop.ini 166 | 167 | # Recycle Bin used on file shares 168 | $RECYCLE.BIN/ 169 | 170 | # Windows Installer files 171 | *.cab 172 | *.msi 173 | *.msix 174 | *.msm 175 | *.msp 176 | 177 | # Windows shortcuts 178 | *.lnk 179 | 180 | # End of https://www.gitignore.io/api/go,linux,macos,windows,intellij+all 181 | 182 | cmd/relay/*.json 183 | hy_linux 184 | .vscode 185 | 186 | /build/ -------------------------------------------------------------------------------- /pkg/tun/server.go: -------------------------------------------------------------------------------- 1 | //go:build gpl 2 | // +build gpl 3 | 4 | package tun 5 | 6 | import ( 7 | "fmt" 8 | "net" 9 | "os" 10 | "os/signal" 11 | "strconv" 12 | "syscall" 13 | "time" 14 | 15 | "github.com/xjasonlyu/tun2socks/v2/core/option" 16 | 17 | "github.com/HyNetwork/hysteria/pkg/core" 18 | "github.com/sirupsen/logrus" 19 | t2score "github.com/xjasonlyu/tun2socks/v2/core" 20 | "github.com/xjasonlyu/tun2socks/v2/core/adapter" 21 | "github.com/xjasonlyu/tun2socks/v2/core/device" 22 | "github.com/xjasonlyu/tun2socks/v2/core/device/fdbased" 23 | "github.com/xjasonlyu/tun2socks/v2/core/device/tun" 24 | "gvisor.dev/gvisor/pkg/tcpip/stack" 25 | ) 26 | 27 | var _ adapter.TransportHandler = (*Server)(nil) 28 | 29 | type Server struct { 30 | HyClient *core.Client 31 | Timeout time.Duration 32 | DeviceInfo DeviceInfo 33 | 34 | RequestFunc func(addr net.Addr, reqAddr string) 35 | ErrorFunc func(addr net.Addr, reqAddr string, err error) 36 | } 37 | 38 | const ( 39 | MTU = 1500 40 | ) 41 | 42 | const ( 43 | DeviceTypeFd = iota 44 | DeviceTypeName 45 | ) 46 | 47 | type DeviceInfo struct { 48 | Type int 49 | Fd int 50 | Name string 51 | MTU uint32 52 | TCPSendBufferSize int 53 | TCPReceiveBufferSize int 54 | TCPModerateReceiveBuffer bool 55 | } 56 | 57 | func (d *DeviceInfo) Open() (dev device.Device, err error) { 58 | switch d.Type { 59 | case DeviceTypeFd: 60 | dev, err = fdbased.Open(strconv.Itoa(d.Fd), d.MTU) 61 | case DeviceTypeName: 62 | dev, err = tun.Open(d.Name, d.MTU) 63 | default: 64 | err = fmt.Errorf("unknown device type: %d", d.Type) 65 | } 66 | return 67 | } 68 | 69 | func NewServerWithTunFd(hyClient *core.Client, timeout time.Duration, tunFd int, mtu uint32, 70 | tcpSendBufferSize, tcpReceiveBufferSize int, tcpModerateReceiveBuffer bool, 71 | ) (*Server, error) { 72 | if mtu == 0 { 73 | mtu = MTU 74 | } 75 | s := &Server{ 76 | HyClient: hyClient, 77 | Timeout: timeout, 78 | DeviceInfo: DeviceInfo{ 79 | Type: DeviceTypeFd, 80 | Fd: tunFd, 81 | MTU: mtu, 82 | TCPSendBufferSize: tcpSendBufferSize, 83 | TCPReceiveBufferSize: tcpReceiveBufferSize, 84 | TCPModerateReceiveBuffer: tcpModerateReceiveBuffer, 85 | }, 86 | } 87 | return s, nil 88 | } 89 | 90 | func NewServer(hyClient *core.Client, timeout time.Duration, name string, mtu uint32, 91 | tcpSendBufferSize, tcpReceiveBufferSize int, tcpModerateReceiveBuffer bool, 92 | ) (*Server, error) { 93 | if mtu == 0 { 94 | mtu = MTU 95 | } 96 | s := &Server{ 97 | HyClient: hyClient, 98 | Timeout: timeout, 99 | DeviceInfo: DeviceInfo{ 100 | Type: DeviceTypeName, 101 | Name: name, 102 | MTU: mtu, 103 | TCPSendBufferSize: tcpSendBufferSize, 104 | TCPReceiveBufferSize: tcpReceiveBufferSize, 105 | TCPModerateReceiveBuffer: tcpModerateReceiveBuffer, 106 | }, 107 | } 108 | return s, nil 109 | } 110 | 111 | func (s *Server) ListenAndServe() error { 112 | var dev device.Device 113 | var st *stack.Stack 114 | 115 | defer func() { 116 | if dev != nil { 117 | _ = dev.Close() 118 | } 119 | if st != nil { 120 | st.Close() 121 | st.Wait() 122 | } 123 | }() 124 | 125 | dev, err := s.DeviceInfo.Open() 126 | if err != nil { 127 | return err 128 | } 129 | 130 | var opts []option.Option 131 | if s.DeviceInfo.TCPSendBufferSize > 0 { 132 | opts = append(opts, option.WithTCPSendBufferSize(s.DeviceInfo.TCPSendBufferSize)) 133 | } 134 | if s.DeviceInfo.TCPReceiveBufferSize > 0 { 135 | opts = append(opts, option.WithTCPReceiveBufferSize(s.DeviceInfo.TCPReceiveBufferSize)) 136 | } 137 | if s.DeviceInfo.TCPModerateReceiveBuffer { 138 | opts = append(opts, option.WithTCPModerateReceiveBuffer(s.DeviceInfo.TCPModerateReceiveBuffer)) 139 | } 140 | 141 | t2sconf := t2score.Config{ 142 | LinkEndpoint: dev, 143 | TransportHandler: s, 144 | PrintFunc: func(format string, v ...interface{}) { 145 | logrus.Infof(format, v...) 146 | }, 147 | Options: opts, 148 | } 149 | 150 | st, err = t2score.CreateStack(&t2sconf) 151 | if err != nil { 152 | return err 153 | } 154 | 155 | sigCh := make(chan os.Signal, 1) 156 | signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) 157 | <-sigCh 158 | 159 | return nil 160 | } 161 | -------------------------------------------------------------------------------- /pkg/congestion/brutal.go: -------------------------------------------------------------------------------- 1 | package congestion 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/lucas-clemente/quic-go/congestion" 7 | ) 8 | 9 | const ( 10 | initMaxDatagramSize = 1252 11 | 12 | pktInfoSlotCount = 4 13 | minSampleCount = 50 14 | minAckRate = 0.8 15 | ) 16 | 17 | type BrutalSender struct { 18 | rttStats congestion.RTTStatsProvider 19 | bps congestion.ByteCount 20 | maxDatagramSize congestion.ByteCount 21 | pacer *pacer 22 | 23 | pktInfoSlots [pktInfoSlotCount]pktInfo 24 | ackRate float64 25 | } 26 | 27 | type pktInfo struct { 28 | Timestamp int64 29 | AckCount uint64 30 | LossCount uint64 31 | } 32 | 33 | func NewBrutalSender(bps congestion.ByteCount) *BrutalSender { 34 | bs := &BrutalSender{ 35 | bps: bps, 36 | maxDatagramSize: initMaxDatagramSize, 37 | ackRate: 1, 38 | } 39 | bs.pacer = newPacer(func() congestion.ByteCount { 40 | return congestion.ByteCount(float64(bs.bps) / bs.ackRate) 41 | }) 42 | return bs 43 | } 44 | 45 | func (b *BrutalSender) SetRTTStatsProvider(rttStats congestion.RTTStatsProvider) { 46 | b.rttStats = rttStats 47 | } 48 | 49 | func (b *BrutalSender) TimeUntilSend(bytesInFlight congestion.ByteCount) time.Time { 50 | return b.pacer.TimeUntilSend() 51 | } 52 | 53 | func (b *BrutalSender) HasPacingBudget() bool { 54 | return b.pacer.Budget(time.Now()) >= b.maxDatagramSize 55 | } 56 | 57 | func (b *BrutalSender) CanSend(bytesInFlight congestion.ByteCount) bool { 58 | return bytesInFlight < b.GetCongestionWindow() 59 | } 60 | 61 | func (b *BrutalSender) GetCongestionWindow() congestion.ByteCount { 62 | rtt := maxDuration(b.rttStats.LatestRTT(), b.rttStats.SmoothedRTT()) 63 | if rtt <= 0 { 64 | return 10240 65 | } 66 | return congestion.ByteCount(float64(b.bps) * rtt.Seconds() * 1.5 / b.ackRate) 67 | } 68 | 69 | func (b *BrutalSender) OnPacketSent(sentTime time.Time, bytesInFlight congestion.ByteCount, 70 | packetNumber congestion.PacketNumber, bytes congestion.ByteCount, isRetransmittable bool, 71 | ) { 72 | b.pacer.SentPacket(sentTime, bytes) 73 | } 74 | 75 | func (b *BrutalSender) OnPacketAcked(number congestion.PacketNumber, ackedBytes congestion.ByteCount, 76 | priorInFlight congestion.ByteCount, eventTime time.Time, 77 | ) { 78 | currentTimestamp := eventTime.Unix() 79 | slot := currentTimestamp % pktInfoSlotCount 80 | if b.pktInfoSlots[slot].Timestamp == currentTimestamp { 81 | b.pktInfoSlots[slot].AckCount++ 82 | } else { 83 | // uninitialized slot or too old, reset 84 | b.pktInfoSlots[slot].Timestamp = currentTimestamp 85 | b.pktInfoSlots[slot].AckCount = 1 86 | b.pktInfoSlots[slot].LossCount = 0 87 | } 88 | b.updateAckRate(currentTimestamp) 89 | } 90 | 91 | func (b *BrutalSender) OnPacketLost(number congestion.PacketNumber, lostBytes congestion.ByteCount, 92 | priorInFlight congestion.ByteCount, 93 | ) { 94 | currentTimestamp := time.Now().Unix() 95 | slot := currentTimestamp % pktInfoSlotCount 96 | if b.pktInfoSlots[slot].Timestamp == currentTimestamp { 97 | b.pktInfoSlots[slot].LossCount++ 98 | } else { 99 | // uninitialized slot or too old, reset 100 | b.pktInfoSlots[slot].Timestamp = currentTimestamp 101 | b.pktInfoSlots[slot].AckCount = 0 102 | b.pktInfoSlots[slot].LossCount = 1 103 | } 104 | b.updateAckRate(currentTimestamp) 105 | } 106 | 107 | func (b *BrutalSender) SetMaxDatagramSize(size congestion.ByteCount) { 108 | b.maxDatagramSize = size 109 | b.pacer.SetMaxDatagramSize(size) 110 | } 111 | 112 | func (b *BrutalSender) updateAckRate(currentTimestamp int64) { 113 | minTimestamp := currentTimestamp - pktInfoSlotCount 114 | var ackCount, lossCount uint64 115 | for _, info := range b.pktInfoSlots { 116 | if info.Timestamp < minTimestamp { 117 | continue 118 | } 119 | ackCount += info.AckCount 120 | lossCount += info.LossCount 121 | } 122 | if ackCount+lossCount < minSampleCount { 123 | b.ackRate = 1 124 | } 125 | rate := float64(ackCount) / float64(ackCount+lossCount) 126 | if rate < minAckRate { 127 | b.ackRate = minAckRate 128 | } 129 | b.ackRate = rate 130 | } 131 | 132 | func (b *BrutalSender) InSlowStart() bool { 133 | return false 134 | } 135 | 136 | func (b *BrutalSender) InRecovery() bool { 137 | return false 138 | } 139 | 140 | func (b *BrutalSender) MaybeExitSlowStart() {} 141 | 142 | func (b *BrutalSender) OnRetransmissionTimeout(packetsRetransmitted bool) {} 143 | 144 | func maxDuration(a, b time.Duration) time.Duration { 145 | if a > b { 146 | return a 147 | } 148 | return b 149 | } 150 | -------------------------------------------------------------------------------- /pkg/conns/faketcp/tcp_test.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package faketcp 5 | 6 | import ( 7 | "log" 8 | "net" 9 | "net/http" 10 | _ "net/http/pprof" 11 | "testing" 12 | ) 13 | 14 | // const testPortStream = "127.0.0.1:3456" 15 | // const testPortPacket = "127.0.0.1:3457" 16 | 17 | const ( 18 | testPortStream = "127.0.0.1:3456" 19 | portServerPacket = "[::]:3457" 20 | portRemotePacket = "127.0.0.1:3457" 21 | ) 22 | 23 | func init() { 24 | startTCPServer() 25 | startTCPRawServer() 26 | go func() { 27 | log.Println(http.ListenAndServe("0.0.0.0:6060", nil)) 28 | }() 29 | } 30 | 31 | func startTCPServer() net.Listener { 32 | l, err := net.Listen("tcp", testPortStream) 33 | if err != nil { 34 | log.Panicln(err) 35 | } 36 | 37 | go func() { 38 | defer l.Close() 39 | for { 40 | conn, err := l.Accept() 41 | if err != nil { 42 | log.Println(err) 43 | return 44 | } 45 | 46 | go handleRequest(conn) 47 | } 48 | }() 49 | return l 50 | } 51 | 52 | func startTCPRawServer() *TCPConn { 53 | conn, err := Listen("tcp", portServerPacket) 54 | if err != nil { 55 | log.Panicln(err) 56 | } 57 | err = conn.SetReadBuffer(1024 * 1024) 58 | if err != nil { 59 | log.Println(err) 60 | } 61 | err = conn.SetWriteBuffer(1024 * 1024) 62 | if err != nil { 63 | log.Println(err) 64 | } 65 | 66 | go func() { 67 | defer conn.Close() 68 | buf := make([]byte, 1024) 69 | for { 70 | n, addr, err := conn.ReadFrom(buf) 71 | if err != nil { 72 | log.Println("server readfrom:", err) 73 | return 74 | } 75 | // echo 76 | n, err = conn.WriteTo(buf[:n], addr) 77 | if err != nil { 78 | log.Println("server writeTo:", err) 79 | return 80 | } 81 | } 82 | }() 83 | return conn 84 | } 85 | 86 | func handleRequest(conn net.Conn) { 87 | defer conn.Close() 88 | 89 | for { 90 | buf := make([]byte, 1024) 91 | size, err := conn.Read(buf) 92 | if err != nil { 93 | log.Println("handleRequest:", err) 94 | return 95 | } 96 | data := buf[:size] 97 | conn.Write(data) 98 | } 99 | } 100 | 101 | func TestDialTCPStream(t *testing.T) { 102 | conn, err := Dial("tcp", testPortStream) 103 | if err != nil { 104 | t.Fatal(err) 105 | } 106 | defer conn.Close() 107 | 108 | addr, err := net.ResolveTCPAddr("tcp", testPortStream) 109 | if err != nil { 110 | t.Fatal(err) 111 | } 112 | 113 | n, err := conn.WriteTo([]byte("abc"), addr) 114 | if err != nil { 115 | t.Fatal(n, err) 116 | } 117 | 118 | buf := make([]byte, 1024) 119 | if n, addr, err := conn.ReadFrom(buf); err != nil { 120 | t.Fatal(n, addr, err) 121 | } else { 122 | log.Println(string(buf[:n]), "from:", addr) 123 | } 124 | } 125 | 126 | func TestDialToTCPPacket(t *testing.T) { 127 | conn, err := Dial("tcp", portRemotePacket) 128 | if err != nil { 129 | t.Fatal(err) 130 | } 131 | defer conn.Close() 132 | 133 | addr, err := net.ResolveTCPAddr("tcp", portRemotePacket) 134 | if err != nil { 135 | t.Fatal(err) 136 | } 137 | 138 | n, err := conn.WriteTo([]byte("abc"), addr) 139 | if err != nil { 140 | t.Fatal(n, err) 141 | } 142 | log.Println("written") 143 | 144 | buf := make([]byte, 1024) 145 | log.Println("readfrom buf") 146 | if n, addr, err := conn.ReadFrom(buf); err != nil { 147 | log.Println(err) 148 | t.Fatal(n, addr, err) 149 | } else { 150 | log.Println(string(buf[:n]), "from:", addr) 151 | } 152 | 153 | log.Println("complete") 154 | } 155 | 156 | func TestSettings(t *testing.T) { 157 | conn, err := Dial("tcp", portRemotePacket) 158 | if err != nil { 159 | t.Fatal(err) 160 | } 161 | defer conn.Close() 162 | if err := conn.SetDSCP(46); err != nil { 163 | log.Fatal("SetDSCP:", err) 164 | } 165 | if err := conn.SetReadBuffer(4096); err != nil { 166 | log.Fatal("SetReaderBuffer:", err) 167 | } 168 | if err := conn.SetWriteBuffer(4096); err != nil { 169 | log.Fatal("SetWriteBuffer:", err) 170 | } 171 | } 172 | 173 | func BenchmarkEcho(b *testing.B) { 174 | conn, err := Dial("tcp", portRemotePacket) 175 | if err != nil { 176 | b.Fatal(err) 177 | } 178 | defer conn.Close() 179 | 180 | addr, err := net.ResolveTCPAddr("tcp", portRemotePacket) 181 | if err != nil { 182 | b.Fatal(err) 183 | } 184 | 185 | buf := make([]byte, 1024) 186 | b.ReportAllocs() 187 | b.SetBytes(int64(len(buf))) 188 | for i := 0; i < b.N; i++ { 189 | n, err := conn.WriteTo(buf, addr) 190 | if err != nil { 191 | b.Fatal(n, err) 192 | } 193 | 194 | if n, addr, err := conn.ReadFrom(buf); err != nil { 195 | b.Fatal(n, addr, err) 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /cmd/client_gpl.go: -------------------------------------------------------------------------------- 1 | //go:build gpl 2 | // +build gpl 3 | 4 | package main 5 | 6 | import ( 7 | "io" 8 | "net" 9 | "strings" 10 | "time" 11 | 12 | "github.com/docker/go-units" 13 | "gvisor.dev/gvisor/pkg/tcpip/transport/tcp" 14 | 15 | "github.com/HyNetwork/hysteria/pkg/core" 16 | "github.com/HyNetwork/hysteria/pkg/tun" 17 | "github.com/sirupsen/logrus" 18 | ) 19 | 20 | const license = `Hysteria is a feature-packed proxy & relay utility optimized for lossy, unstable connections. 21 | Copyright (C) 2022 Toby 22 | 23 | This program is free software: you can redistribute it and/or modify 24 | it under the terms of the GNU General Public License as published by 25 | the Free Software Foundation, either version 3 of the License, or 26 | (at your option) any later version. 27 | 28 | This program is distributed in the hope that it will be useful, 29 | but WITHOUT ANY WARRANTY; without even the implied warranty of 30 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 31 | GNU General Public License for more details. 32 | 33 | You should have received a copy of the GNU General Public License 34 | along with this program. If not, see . 35 | ` 36 | 37 | func startTUN(config *clientConfig, client *core.Client, errChan chan error) { 38 | timeout := time.Duration(config.TUN.Timeout) * time.Second 39 | if timeout == 0 { 40 | timeout = 300 * time.Second 41 | } 42 | 43 | var err error 44 | var tcpSendBufferSize, tcpReceiveBufferSize int64 45 | 46 | if config.TUN.TCPSendBufferSize != "" { 47 | tcpSendBufferSize, err = units.RAMInBytes(config.TUN.TCPSendBufferSize) 48 | if err != nil { 49 | logrus.WithFields(logrus.Fields{ 50 | "err": err, 51 | "tcp-sndbuf": config.TUN.TCPSendBufferSize, 52 | }).Fatal("Failed to parse tcp-sndbuf in the TUN config") 53 | } 54 | if (tcpSendBufferSize != 0 && tcpSendBufferSize < tcp.MinBufferSize) || tcpSendBufferSize > tcp.MaxBufferSize { 55 | logrus.WithFields(logrus.Fields{ 56 | "tcp-sndbuf": config.TUN.TCPSendBufferSize, 57 | }).Fatal("Invalid tcp-sndbuf in the TUN config") 58 | } 59 | } 60 | if config.TUN.TCPReceiveBufferSize != "" { 61 | tcpReceiveBufferSize, err = units.RAMInBytes(config.TUN.TCPReceiveBufferSize) 62 | if err != nil { 63 | logrus.WithFields(logrus.Fields{ 64 | "err": err, 65 | "tcp-rcvbuf": config.TUN.TCPReceiveBufferSize, 66 | }).Fatal("Failed to parse tcp-rcvbuf in the TUN config") 67 | } 68 | if (tcpReceiveBufferSize != 0 && tcpReceiveBufferSize < tcp.MinBufferSize) || tcpReceiveBufferSize > tcp.MaxBufferSize { 69 | logrus.WithFields(logrus.Fields{ 70 | "err": err, 71 | "tcp-rcvbuf": config.TUN.TCPReceiveBufferSize, 72 | }).Fatal("Invalid tcp-rcvbuf in the TUN config") 73 | } 74 | } 75 | 76 | tunServer, err := tun.NewServer(client, time.Duration(config.TUN.Timeout)*time.Second, 77 | config.TUN.Name, config.TUN.MTU, 78 | int(tcpSendBufferSize), int(tcpReceiveBufferSize), config.TUN.TCPModerateReceiveBuffer) 79 | if err != nil { 80 | logrus.WithField("error", err).Fatal("Failed to initialize TUN server") 81 | } 82 | tunServer.RequestFunc = func(addr net.Addr, reqAddr string) { 83 | logrus.WithFields(logrus.Fields{ 84 | "src": defaultIPMasker.Mask(addr.String()), 85 | "dst": defaultIPMasker.Mask(reqAddr), 86 | }).Debugf("TUN %s request", strings.ToUpper(addr.Network())) 87 | } 88 | tunServer.ErrorFunc = func(addr net.Addr, reqAddr string, err error) { 89 | if err != nil { 90 | if err == io.EOF { 91 | logrus.WithFields(logrus.Fields{ 92 | "src": defaultIPMasker.Mask(addr.String()), 93 | "dst": defaultIPMasker.Mask(reqAddr), 94 | }).Debugf("TUN %s EOF", strings.ToUpper(addr.Network())) 95 | } else if err == core.ErrClosed && strings.HasPrefix(addr.Network(), "udp") { 96 | logrus.WithFields(logrus.Fields{ 97 | "src": defaultIPMasker.Mask(addr.String()), 98 | "dst": defaultIPMasker.Mask(reqAddr), 99 | }).Debugf("TUN %s closed for timeout", strings.ToUpper(addr.Network())) 100 | } else if nErr, ok := err.(net.Error); ok && nErr.Timeout() && strings.HasPrefix(addr.Network(), "tcp") { 101 | logrus.WithFields(logrus.Fields{ 102 | "src": defaultIPMasker.Mask(addr.String()), 103 | "dst": defaultIPMasker.Mask(reqAddr), 104 | }).Debugf("TUN %s closed for timeout", strings.ToUpper(addr.Network())) 105 | } else { 106 | logrus.WithFields(logrus.Fields{ 107 | "error": err, 108 | "src": defaultIPMasker.Mask(addr.String()), 109 | "dst": defaultIPMasker.Mask(reqAddr), 110 | }).Infof("TUN %s error", strings.ToUpper(addr.Network())) 111 | } 112 | } 113 | } 114 | logrus.WithField("interface", config.TUN.Name).Info("TUN up and running") 115 | errChan <- tunServer.ListenAndServe() 116 | } 117 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/HyNetwork/hysteria 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/LiamHaworth/go-tproxy v0.0.0-20190726054950-ef7efd7f24ed 7 | github.com/antonfisher/nested-logrus-formatter v1.3.1 8 | github.com/caddyserver/certmagic v0.17.1 9 | github.com/coreos/go-iptables v0.6.0 10 | github.com/docker/go-units v0.4.0 11 | github.com/elazarl/goproxy v0.0.0-20220115173737-adb46da277ac 12 | github.com/elazarl/goproxy/ext v0.0.0-20220115173737-adb46da277ac 13 | github.com/folbricht/routedns v0.1.6-0.20220806202012-361f5b35b4c3 14 | github.com/fsnotify/fsnotify v1.5.4 15 | github.com/google/gopacket v1.1.19 16 | github.com/hashicorp/golang-lru v0.5.4 17 | github.com/lucas-clemente/quic-go v0.28.1 18 | github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 19 | github.com/oschwald/geoip2-golang v1.8.0 20 | github.com/prometheus/client_golang v1.13.0 21 | github.com/sirupsen/logrus v1.9.0 22 | github.com/spf13/cobra v1.5.0 23 | github.com/spf13/viper v1.13.0 24 | github.com/txthinking/socks5 v0.0.0-20220212043548-414499347d4a 25 | github.com/xjasonlyu/tun2socks/v2 v2.4.1 26 | github.com/yosuke-furukawa/json5 v0.1.1 27 | golang.org/x/sys v0.0.0-20220804214406-8e32c043e418 28 | gvisor.dev/gvisor v0.0.0-20220405222207-795f4f0139bb 29 | ) 30 | 31 | require ( 32 | github.com/RackSec/srslog v0.0.0-20180709174129-a4725f04ec91 // indirect 33 | github.com/beorn7/perks v1.0.1 // indirect 34 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 35 | github.com/cheekybits/genny v1.0.0 // indirect 36 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect 37 | github.com/golang/protobuf v1.5.2 // indirect 38 | github.com/google/btree v1.0.1 // indirect 39 | github.com/hashicorp/hcl v1.0.0 // indirect 40 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 41 | github.com/jtacoma/uritemplates v1.0.0 // indirect 42 | github.com/klauspost/cpuid/v2 v2.1.0 // indirect 43 | github.com/libdns/libdns v0.2.1 // indirect 44 | github.com/magiconair/properties v1.8.6 // indirect 45 | github.com/marten-seemann/qpack v0.2.1 // indirect 46 | github.com/marten-seemann/qtls-go1-16 v0.1.5 // indirect 47 | github.com/marten-seemann/qtls-go1-17 v0.1.2 // indirect 48 | github.com/marten-seemann/qtls-go1-18 v0.1.2 // indirect 49 | github.com/marten-seemann/qtls-go1-19 v0.1.0-beta.1 // indirect 50 | github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect 51 | github.com/mholt/acmez v1.0.4 // indirect 52 | github.com/miekg/dns v1.1.50 // indirect 53 | github.com/mitchellh/mapstructure v1.5.0 // indirect 54 | github.com/nxadm/tail v1.4.8 // indirect 55 | github.com/onsi/ginkgo v1.16.4 // indirect 56 | github.com/oschwald/maxminddb-golang v1.10.0 // indirect 57 | github.com/patrickmn/go-cache v2.1.0+incompatible // indirect 58 | github.com/pelletier/go-toml v1.9.5 // indirect 59 | github.com/pelletier/go-toml/v2 v2.0.5 // indirect 60 | github.com/pion/dtls/v2 v2.1.3 // indirect 61 | github.com/pion/logging v0.2.2 // indirect 62 | github.com/pion/transport v0.13.0 // indirect 63 | github.com/pion/udp v0.1.1 // indirect 64 | github.com/pkg/errors v0.9.1 // indirect 65 | github.com/prometheus/client_model v0.2.0 // indirect 66 | github.com/prometheus/common v0.37.0 // indirect 67 | github.com/prometheus/procfs v0.8.0 // indirect 68 | github.com/spf13/afero v1.8.2 // indirect 69 | github.com/spf13/cast v1.5.0 // indirect 70 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 71 | github.com/spf13/pflag v1.0.5 // indirect 72 | github.com/subosito/gotenv v1.4.1 // indirect 73 | github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf // indirect 74 | github.com/txthinking/x v0.0.0-20210326105829-476fab902fbe // indirect 75 | go.uber.org/atomic v1.9.0 // indirect 76 | go.uber.org/multierr v1.6.0 // indirect 77 | go.uber.org/zap v1.21.0 // indirect 78 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect 79 | golang.org/x/mod v0.5.1 // indirect 80 | golang.org/x/net v0.0.0-20220805013720-a33c5aa5df48 // indirect 81 | golang.org/x/text v0.3.7 // indirect 82 | golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 // indirect 83 | golang.org/x/tools v0.1.9 // indirect 84 | golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df // indirect 85 | golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 // indirect 86 | golang.zx2c4.com/wireguard v0.0.0-20220318042302-193cf8d6a5d6 // indirect 87 | google.golang.org/protobuf v1.28.1 // indirect 88 | gopkg.in/ini.v1 v1.67.0 // indirect 89 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect 90 | gopkg.in/yaml.v2 v2.4.0 // indirect 91 | gopkg.in/yaml.v3 v3.0.1 // indirect 92 | ) 93 | 94 | replace github.com/lucas-clemente/quic-go => github.com/HyNetwork/quic-go v0.28.2-0.20220806194731-5be744e08984 95 | 96 | replace github.com/LiamHaworth/go-tproxy => github.com/HyNetwork/go-tproxy v0.0.0-20220916084518-d32f8ab1e8e9 97 | -------------------------------------------------------------------------------- /pkg/transport/server.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "net" 7 | "strconv" 8 | "time" 9 | 10 | "github.com/HyNetwork/hysteria/pkg/conns/faketcp" 11 | "github.com/HyNetwork/hysteria/pkg/conns/udp" 12 | "github.com/HyNetwork/hysteria/pkg/conns/wechat" 13 | obfsPkg "github.com/HyNetwork/hysteria/pkg/obfs" 14 | "github.com/HyNetwork/hysteria/pkg/sockopt" 15 | "github.com/HyNetwork/hysteria/pkg/utils" 16 | "github.com/lucas-clemente/quic-go" 17 | ) 18 | 19 | type ServerTransport struct { 20 | Dialer *net.Dialer 21 | SOCKS5Client *SOCKS5Client 22 | ResolvePreference ResolvePreference 23 | LocalUDPAddr *net.UDPAddr 24 | LocalUDPIntf *net.Interface 25 | } 26 | 27 | // AddrEx is like net.TCPAddr or net.UDPAddr, but with additional domain information for SOCKS5. 28 | // At least one of Domain and IPAddr must be non-empty. 29 | type AddrEx struct { 30 | Domain string 31 | IPAddr *net.IPAddr 32 | Port int 33 | } 34 | 35 | func (a *AddrEx) String() string { 36 | if a == nil { 37 | return "" 38 | } 39 | var ip string 40 | if a.IPAddr != nil { 41 | ip = a.IPAddr.String() 42 | } 43 | return net.JoinHostPort(ip, strconv.Itoa(a.Port)) 44 | } 45 | 46 | type PUDPConn interface { 47 | ReadFromUDP([]byte) (int, *net.UDPAddr, error) 48 | WriteToUDP([]byte, *AddrEx) (int, error) 49 | Close() error 50 | } 51 | 52 | type udpConnPUDPConn struct { 53 | Conn *net.UDPConn 54 | } 55 | 56 | func (c *udpConnPUDPConn) ReadFromUDP(bytes []byte) (int, *net.UDPAddr, error) { 57 | return c.Conn.ReadFromUDP(bytes) 58 | } 59 | 60 | func (c *udpConnPUDPConn) WriteToUDP(bytes []byte, ex *AddrEx) (int, error) { 61 | return c.Conn.WriteToUDP(bytes, &net.UDPAddr{ 62 | IP: ex.IPAddr.IP, 63 | Port: ex.Port, 64 | Zone: ex.IPAddr.Zone, 65 | }) 66 | } 67 | 68 | func (c *udpConnPUDPConn) Close() error { 69 | return c.Conn.Close() 70 | } 71 | 72 | var DefaultServerTransport = &ServerTransport{ 73 | Dialer: &net.Dialer{ 74 | Timeout: 8 * time.Second, 75 | }, 76 | ResolvePreference: ResolvePreferenceDefault, 77 | } 78 | 79 | func (st *ServerTransport) quicPacketConn(proto string, laddr string, obfs obfsPkg.Obfuscator) (net.PacketConn, error) { 80 | if len(proto) == 0 || proto == "udp" { 81 | laddrU, err := net.ResolveUDPAddr("udp", laddr) 82 | if err != nil { 83 | return nil, err 84 | } 85 | conn, err := net.ListenUDP("udp", laddrU) 86 | if err != nil { 87 | return nil, err 88 | } 89 | if obfs != nil { 90 | oc := udp.NewObfsUDPConn(conn, obfs) 91 | return oc, nil 92 | } else { 93 | return conn, nil 94 | } 95 | } else if proto == "wechat-video" { 96 | laddrU, err := net.ResolveUDPAddr("udp", laddr) 97 | if err != nil { 98 | return nil, err 99 | } 100 | conn, err := net.ListenUDP("udp", laddrU) 101 | if err != nil { 102 | return nil, err 103 | } 104 | if obfs == nil { 105 | obfs = obfsPkg.NewDummyObfuscator() 106 | } 107 | return wechat.NewObfsWeChatUDPConn(conn, obfs), nil 108 | } else if proto == "faketcp" { 109 | conn, err := faketcp.Listen("tcp", laddr) 110 | if err != nil { 111 | return nil, err 112 | } 113 | if obfs != nil { 114 | oc := faketcp.NewObfsFakeTCPConn(conn, obfs) 115 | return oc, nil 116 | } else { 117 | return conn, nil 118 | } 119 | } else { 120 | return nil, fmt.Errorf("unsupported protocol: %s", proto) 121 | } 122 | } 123 | 124 | func (st *ServerTransport) QUICListen(proto string, listen string, tlsConfig *tls.Config, quicConfig *quic.Config, obfs obfsPkg.Obfuscator) (quic.Listener, error) { 125 | pktConn, err := st.quicPacketConn(proto, listen, obfs) 126 | if err != nil { 127 | return nil, err 128 | } 129 | l, err := quic.Listen(pktConn, tlsConfig, quicConfig) 130 | if err != nil { 131 | _ = pktConn.Close() 132 | return nil, err 133 | } 134 | return l, nil 135 | } 136 | 137 | func (st *ServerTransport) ResolveIPAddr(address string) (*net.IPAddr, bool, error) { 138 | ip, zone := utils.ParseIPZone(address) 139 | if ip != nil { 140 | return &net.IPAddr{IP: ip, Zone: zone}, false, nil 141 | } 142 | ipAddr, err := resolveIPAddrWithPreference(address, st.ResolvePreference) 143 | return ipAddr, true, err 144 | } 145 | 146 | func (st *ServerTransport) DialTCP(raddr *AddrEx) (*net.TCPConn, error) { 147 | if st.SOCKS5Client != nil { 148 | return st.SOCKS5Client.DialTCP(raddr) 149 | } else { 150 | conn, err := st.Dialer.Dial("tcp", raddr.String()) 151 | if err != nil { 152 | return nil, err 153 | } 154 | return conn.(*net.TCPConn), nil 155 | } 156 | } 157 | 158 | func (st *ServerTransport) ListenUDP() (PUDPConn, error) { 159 | if st.SOCKS5Client != nil { 160 | return st.SOCKS5Client.ListenUDP() 161 | } else { 162 | conn, err := net.ListenUDP("udp", st.LocalUDPAddr) 163 | if err != nil { 164 | return nil, err 165 | } 166 | if st.LocalUDPIntf != nil { 167 | err = sockopt.BindUDPConn("udp", conn, st.LocalUDPIntf) 168 | if err != nil { 169 | conn.Close() 170 | return nil, err 171 | } 172 | } 173 | return &udpConnPUDPConn{ 174 | Conn: conn, 175 | }, nil 176 | } 177 | } 178 | 179 | func (st *ServerTransport) SOCKS5Enabled() bool { 180 | return st.SOCKS5Client != nil 181 | } 182 | -------------------------------------------------------------------------------- /pkg/core/server.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "errors" 7 | "fmt" 8 | "net" 9 | 10 | "github.com/HyNetwork/hysteria/pkg/acl" 11 | "github.com/HyNetwork/hysteria/pkg/obfs" 12 | "github.com/HyNetwork/hysteria/pkg/pmtud_fix" 13 | "github.com/HyNetwork/hysteria/pkg/transport" 14 | "github.com/lucas-clemente/quic-go" 15 | "github.com/lunixbochs/struc" 16 | "github.com/prometheus/client_golang/prometheus" 17 | ) 18 | 19 | type ( 20 | ConnectFunc func(addr net.Addr, auth []byte, sSend uint64, sRecv uint64) (bool, string) 21 | DisconnectFunc func(addr net.Addr, auth []byte, err error) 22 | TCPRequestFunc func(addr net.Addr, auth []byte, reqAddr string, action acl.Action, arg string) 23 | TCPErrorFunc func(addr net.Addr, auth []byte, reqAddr string, err error) 24 | UDPRequestFunc func(addr net.Addr, auth []byte, sessionID uint32) 25 | UDPErrorFunc func(addr net.Addr, auth []byte, sessionID uint32, err error) 26 | ) 27 | 28 | type Server struct { 29 | transport *transport.ServerTransport 30 | sendBPS, recvBPS uint64 31 | congestionFactory CongestionFactory 32 | disableUDP bool 33 | aclEngine *acl.Engine 34 | 35 | connectFunc ConnectFunc 36 | disconnectFunc DisconnectFunc 37 | tcpRequestFunc TCPRequestFunc 38 | tcpErrorFunc TCPErrorFunc 39 | udpRequestFunc UDPRequestFunc 40 | udpErrorFunc UDPErrorFunc 41 | 42 | upCounterVec, downCounterVec *prometheus.CounterVec 43 | connGaugeVec *prometheus.GaugeVec 44 | 45 | listener quic.Listener 46 | } 47 | 48 | func NewServer(addr string, protocol string, tlsConfig *tls.Config, quicConfig *quic.Config, transport *transport.ServerTransport, 49 | sendBPS uint64, recvBPS uint64, congestionFactory CongestionFactory, disableUDP bool, aclEngine *acl.Engine, 50 | obfuscator obfs.Obfuscator, connectFunc ConnectFunc, disconnectFunc DisconnectFunc, 51 | tcpRequestFunc TCPRequestFunc, tcpErrorFunc TCPErrorFunc, 52 | udpRequestFunc UDPRequestFunc, udpErrorFunc UDPErrorFunc, promRegistry *prometheus.Registry, 53 | ) (*Server, error) { 54 | quicConfig.DisablePathMTUDiscovery = quicConfig.DisablePathMTUDiscovery || pmtud_fix.DisablePathMTUDiscovery 55 | listener, err := transport.QUICListen(protocol, addr, tlsConfig, quicConfig, obfuscator) 56 | if err != nil { 57 | return nil, err 58 | } 59 | s := &Server{ 60 | listener: listener, 61 | transport: transport, 62 | sendBPS: sendBPS, 63 | recvBPS: recvBPS, 64 | congestionFactory: congestionFactory, 65 | disableUDP: disableUDP, 66 | aclEngine: aclEngine, 67 | connectFunc: connectFunc, 68 | disconnectFunc: disconnectFunc, 69 | tcpRequestFunc: tcpRequestFunc, 70 | tcpErrorFunc: tcpErrorFunc, 71 | udpRequestFunc: udpRequestFunc, 72 | udpErrorFunc: udpErrorFunc, 73 | } 74 | if promRegistry != nil { 75 | s.upCounterVec = prometheus.NewCounterVec(prometheus.CounterOpts{ 76 | Name: "hysteria_traffic_uplink_bytes_total", 77 | }, []string{"auth"}) 78 | s.downCounterVec = prometheus.NewCounterVec(prometheus.CounterOpts{ 79 | Name: "hysteria_traffic_downlink_bytes_total", 80 | }, []string{"auth"}) 81 | s.connGaugeVec = prometheus.NewGaugeVec(prometheus.GaugeOpts{ 82 | Name: "hysteria_active_conn", 83 | }, []string{"auth"}) 84 | promRegistry.MustRegister(s.upCounterVec, s.downCounterVec, s.connGaugeVec) 85 | } 86 | return s, nil 87 | } 88 | 89 | func (s *Server) Serve() error { 90 | for { 91 | cs, err := s.listener.Accept(context.Background()) 92 | if err != nil { 93 | return err 94 | } 95 | go s.handleClient(cs) 96 | } 97 | } 98 | 99 | func (s *Server) Close() error { 100 | return s.listener.Close() 101 | } 102 | 103 | func (s *Server) handleClient(cs quic.Connection) { 104 | // Expect the client to create a control stream to send its own information 105 | ctx, ctxCancel := context.WithTimeout(context.Background(), protocolTimeout) 106 | stream, err := cs.AcceptStream(ctx) 107 | ctxCancel() 108 | if err != nil { 109 | _ = cs.CloseWithError(closeErrorCodeProtocol, "protocol error") 110 | return 111 | } 112 | // Handle the control stream 113 | auth, ok, v2, err := s.handleControlStream(cs, stream) 114 | if err != nil { 115 | _ = cs.CloseWithError(closeErrorCodeProtocol, "protocol error") 116 | return 117 | } 118 | if !ok { 119 | _ = cs.CloseWithError(closeErrorCodeAuth, "auth error") 120 | return 121 | } 122 | // Start accepting streams and messages 123 | sc := newServerClient(v2, cs, s.transport, auth, s.disableUDP, s.aclEngine, 124 | s.tcpRequestFunc, s.tcpErrorFunc, s.udpRequestFunc, s.udpErrorFunc, 125 | s.upCounterVec, s.downCounterVec, s.connGaugeVec) 126 | err = sc.Run() 127 | _ = cs.CloseWithError(closeErrorCodeGeneric, "") 128 | s.disconnectFunc(cs.RemoteAddr(), auth, err) 129 | } 130 | 131 | // Auth & negotiate speed 132 | func (s *Server) handleControlStream(cs quic.Connection, stream quic.Stream) ([]byte, bool, bool, error) { 133 | // Check version 134 | vb := make([]byte, 1) 135 | _, err := stream.Read(vb) 136 | if err != nil { 137 | return nil, false, false, err 138 | } 139 | if vb[0] != protocolVersion && vb[0] != protocolVersionV2 { 140 | return nil, false, false, fmt.Errorf("unsupported protocol version %d, expecting %d/%d", 141 | vb[0], protocolVersionV2, protocolVersion) 142 | } 143 | // Parse client hello 144 | var ch clientHello 145 | err = struc.Unpack(stream, &ch) 146 | if err != nil { 147 | return nil, false, false, err 148 | } 149 | // Speed 150 | if ch.Rate.SendBPS == 0 || ch.Rate.RecvBPS == 0 { 151 | return nil, false, false, errors.New("invalid rate from client") 152 | } 153 | serverSendBPS, serverRecvBPS := ch.Rate.RecvBPS, ch.Rate.SendBPS 154 | if s.sendBPS > 0 && serverSendBPS > s.sendBPS { 155 | serverSendBPS = s.sendBPS 156 | } 157 | if s.recvBPS > 0 && serverRecvBPS > s.recvBPS { 158 | serverRecvBPS = s.recvBPS 159 | } 160 | // Auth 161 | ok, msg := s.connectFunc(cs.RemoteAddr(), ch.Auth, serverSendBPS, serverRecvBPS) 162 | // Response 163 | err = struc.Pack(stream, &serverHello{ 164 | OK: ok, 165 | Rate: transmissionRate{ 166 | SendBPS: serverSendBPS, 167 | RecvBPS: serverRecvBPS, 168 | }, 169 | Message: msg, 170 | }) 171 | if err != nil { 172 | return nil, false, false, err 173 | } 174 | // Set the congestion accordingly 175 | if ok && s.congestionFactory != nil { 176 | cs.SetCongestionControl(s.congestionFactory(serverSendBPS)) 177 | } 178 | return ch.Auth, ok, vb[0] == protocolVersionV2, nil 179 | } 180 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "math/rand" 7 | "net" 8 | "os" 9 | "regexp" 10 | "strings" 11 | "time" 12 | 13 | nested "github.com/antonfisher/nested-logrus-formatter" 14 | "github.com/sirupsen/logrus" 15 | "github.com/spf13/cobra" 16 | "github.com/spf13/viper" 17 | ) 18 | 19 | const ( 20 | logo = ` 21 | ░█░█░█░█░█▀▀░▀█▀░█▀▀░█▀▄░▀█▀░█▀█ 22 | ░█▀█░░█░░▀▀█░░█░░█▀▀░█▀▄░░█░░█▀█ 23 | ░▀░▀░░▀░░▀▀▀░░▀░░▀▀▀░▀░▀░▀▀▀░▀░▀ 24 | 25 | ` 26 | desc = "A TCP/UDP relay & SOCKS5/HTTP proxy tool optimized for poor network environments" 27 | authors = "HyNetwork " 28 | ) 29 | 30 | var ( 31 | appVersion = "Unknown" 32 | appCommit = "Unknown" 33 | appDate = "Unknown" 34 | ) 35 | 36 | var rootCmd = &cobra.Command{ 37 | Use: "hysteria", 38 | Long: fmt.Sprintf("%s%s\n\nVersion:\t%s\nBuildDate:\t%s\nCommitHash:\t%s\nAuthors:\t%s", logo, desc, appVersion, appDate, appCommit, authors), 39 | Example: "./hysteria server --config /etc/hysteria.json", 40 | Version: fmt.Sprintf("%s %s %s", appVersion, appDate, appCommit), 41 | PersistentPreRun: func(cmd *cobra.Command, args []string) { 42 | rand.Seed(time.Now().UnixNano()) 43 | 44 | // log config 45 | logrus.SetOutput(os.Stdout) 46 | if lvl, err := logrus.ParseLevel(viper.GetString("log-level")); err == nil { 47 | logrus.SetLevel(lvl) 48 | } else { 49 | logrus.SetLevel(logrus.DebugLevel) 50 | } 51 | 52 | if strings.ToLower(viper.GetString("log-format")) == "json" { 53 | logrus.SetFormatter(&logrus.JSONFormatter{ 54 | TimestampFormat: viper.GetString("log-timestamp"), 55 | }) 56 | } else { 57 | logrus.SetFormatter(&nested.Formatter{ 58 | FieldsOrder: []string{ 59 | "version", "url", 60 | "config", "file", "mode", 61 | "addr", "src", "dst", "session", "action", "interface", 62 | "retry", "interval", 63 | "code", "msg", "error", 64 | }, 65 | TimestampFormat: viper.GetString("log-timestamp"), 66 | }) 67 | } 68 | 69 | // license 70 | if viper.GetBool("license") { 71 | fmt.Printf("%s\n", license) 72 | os.Exit(0) 73 | } 74 | 75 | // ip mask config 76 | v4m := viper.GetUint("log-ipv4-mask") 77 | if v4m > 0 && v4m < 32 { 78 | defaultIPMasker.IPv4Mask = net.CIDRMask(int(v4m), 32) 79 | } 80 | v6m := viper.GetUint("log-ipv6-mask") 81 | if v6m > 0 && v6m < 128 { 82 | defaultIPMasker.IPv6Mask = net.CIDRMask(int(v6m), 128) 83 | } 84 | 85 | // check update 86 | if !viper.GetBool("no-check") { 87 | go checkUpdate() 88 | } 89 | }, 90 | Run: func(cmd *cobra.Command, args []string) { 91 | clientCmd.Run(cmd, args) 92 | }, 93 | } 94 | 95 | var clientCmd = &cobra.Command{ 96 | Use: "client", 97 | Short: "Run as client mode", 98 | Example: "./hysteria client --config /etc/hysteria/client.json", 99 | Run: func(cmd *cobra.Command, args []string) { 100 | cbs, err := ioutil.ReadFile(viper.GetString("config")) 101 | if err != nil { 102 | logrus.WithFields(logrus.Fields{ 103 | "file": viper.GetString("config"), 104 | "error": err, 105 | }).Fatal("Failed to read configuration") 106 | } 107 | // client mode 108 | cc, err := parseClientConfig(cbs) 109 | if err != nil { 110 | logrus.WithFields(logrus.Fields{ 111 | "file": viper.GetString("config"), 112 | "error": err, 113 | }).Fatal("Failed to parse client configuration") 114 | } 115 | client(cc) 116 | }, 117 | } 118 | 119 | var serverCmd = &cobra.Command{ 120 | Use: "server", 121 | Short: "Run as server mode", 122 | Example: "./hysteria server --config /etc/hysteria/server.json", 123 | Run: func(cmd *cobra.Command, args []string) { 124 | cbs, err := ioutil.ReadFile(viper.GetString("config")) 125 | if err != nil { 126 | logrus.WithFields(logrus.Fields{ 127 | "file": viper.GetString("config"), 128 | "error": err, 129 | }).Fatal("Failed to read configuration") 130 | } 131 | // server mode 132 | sc, err := parseServerConfig(cbs) 133 | if err != nil { 134 | logrus.WithFields(logrus.Fields{ 135 | "file": viper.GetString("config"), 136 | "error": err, 137 | }).Fatal("Failed to parse server configuration") 138 | } 139 | server(sc) 140 | }, 141 | } 142 | 143 | // fakeFlags replace the old flag format with the new format(eg: `-config` ->> `--config`) 144 | func fakeFlags() { 145 | var args []string 146 | fr, _ := regexp.Compile(`^-[a-zA-Z]{2,}`) 147 | for _, arg := range os.Args { 148 | if fr.MatchString(arg) { 149 | args = append(args, "-"+arg) 150 | } else { 151 | args = append(args, arg) 152 | } 153 | } 154 | os.Args = args 155 | } 156 | 157 | func init() { 158 | // compatible with old flag format 159 | fakeFlags() 160 | 161 | // compatible windows double click 162 | cobra.MousetrapHelpText = "" 163 | 164 | // disable cmd sorting 165 | cobra.EnableCommandSorting = false 166 | 167 | // add global flags 168 | rootCmd.PersistentFlags().StringP("config", "c", "./config.json", "config file") 169 | rootCmd.PersistentFlags().String("mmdb-url", "https://github.com/P3TERX/GeoLite.mmdb/raw/download/GeoLite2-Country.mmdb", "mmdb download url") 170 | rootCmd.PersistentFlags().String("log-level", "debug", "log level") 171 | rootCmd.PersistentFlags().String("log-timestamp", time.RFC3339, "log timestamp format") 172 | rootCmd.PersistentFlags().String("log-format", "txt", "log output format (txt/json)") 173 | rootCmd.PersistentFlags().Uint("log-ipv4-mask", 0, "mask IPv4 addresses in log using a CIDR mask") 174 | rootCmd.PersistentFlags().Uint("log-ipv6-mask", 0, "mask IPv6 addresses in log using a CIDR mask") 175 | rootCmd.PersistentFlags().Bool("no-check", false, "disable update check") 176 | rootCmd.PersistentFlags().Bool("license", false, "show license and exit") 177 | 178 | // add to root cmd 179 | rootCmd.AddCommand(clientCmd, serverCmd, completionCmd) 180 | 181 | // bind flag 182 | _ = viper.BindPFlag("config", rootCmd.PersistentFlags().Lookup("config")) 183 | _ = viper.BindPFlag("mmdb-url", rootCmd.PersistentFlags().Lookup("mmdb-url")) 184 | _ = viper.BindPFlag("log-level", rootCmd.PersistentFlags().Lookup("log-level")) 185 | _ = viper.BindPFlag("log-timestamp", rootCmd.PersistentFlags().Lookup("log-timestamp")) 186 | _ = viper.BindPFlag("log-format", rootCmd.PersistentFlags().Lookup("log-format")) 187 | _ = viper.BindPFlag("log-ipv4-mask", rootCmd.PersistentFlags().Lookup("log-ipv4-mask")) 188 | _ = viper.BindPFlag("log-ipv6-mask", rootCmd.PersistentFlags().Lookup("log-ipv6-mask")) 189 | _ = viper.BindPFlag("no-check", rootCmd.PersistentFlags().Lookup("no-check")) 190 | _ = viper.BindPFlag("license", rootCmd.PersistentFlags().Lookup("license")) 191 | 192 | // bind env 193 | _ = viper.BindEnv("config", "HYSTERIA_CONFIG") 194 | _ = viper.BindEnv("mmdb-url", "HYSTERIA_MMDB_URL") 195 | _ = viper.BindEnv("log-level", "HYSTERIA_LOG_LEVEL", "LOGGING_LEVEL") 196 | _ = viper.BindEnv("log-timestamp", "HYSTERIA_LOG_TIMESTAMP", "LOGGING_TIMESTAMP_FORMAT") 197 | _ = viper.BindEnv("log-format", "HYSTERIA_LOG_FORMAT", "LOGGING_FORMATTER") 198 | _ = viper.BindEnv("log-ipv4-mask", "HYSTERIA_LOG_IPV4_MASK", "LOGGING_IPV4_MASK") 199 | _ = viper.BindEnv("log-ipv6-mask", "HYSTERIA_LOG_IPV6_MASK", "LOGGING_IPV6_MASK") 200 | _ = viper.BindEnv("no-check", "HYSTERIA_NO_CHECK", "HYSTERIA_NO_CHECK_UPDATE") 201 | viper.AutomaticEnv() 202 | } 203 | 204 | func main() { 205 | cobra.CheckErr(rootCmd.Execute()) 206 | } 207 | -------------------------------------------------------------------------------- /pkg/transport/socks5.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "fmt" 7 | "net" 8 | "time" 9 | 10 | "github.com/txthinking/socks5" 11 | ) 12 | 13 | type SOCKS5Client struct { 14 | ServerTCPAddr *net.TCPAddr 15 | Username string 16 | Password string 17 | NegTimeout time.Duration 18 | } 19 | 20 | func NewSOCKS5Client(serverAddr string, username string, password string, negTimeout time.Duration) (*SOCKS5Client, error) { 21 | tcpAddr, err := net.ResolveTCPAddr("tcp", serverAddr) 22 | if err != nil { 23 | return nil, err 24 | } 25 | return &SOCKS5Client{ 26 | ServerTCPAddr: tcpAddr, 27 | Username: username, 28 | Password: password, 29 | NegTimeout: negTimeout, 30 | }, nil 31 | } 32 | 33 | func (c *SOCKS5Client) negotiate(conn *net.TCPConn) error { 34 | m := []byte{socks5.MethodNone} 35 | if c.Username != "" && c.Password != "" { 36 | m = append(m, socks5.MethodUsernamePassword) 37 | } 38 | rq := socks5.NewNegotiationRequest(m) 39 | _, err := rq.WriteTo(conn) 40 | if err != nil { 41 | return err 42 | } 43 | rs, err := socks5.NewNegotiationReplyFrom(conn) 44 | if err != nil { 45 | return err 46 | } 47 | if rs.Method == socks5.MethodUsernamePassword { 48 | urq := socks5.NewUserPassNegotiationRequest([]byte(c.Username), []byte(c.Password)) 49 | _, err = urq.WriteTo(conn) 50 | if err != nil { 51 | return err 52 | } 53 | urs, err := socks5.NewUserPassNegotiationReplyFrom(conn) 54 | if err != nil { 55 | return err 56 | } 57 | if urs.Status != socks5.UserPassStatusSuccess { 58 | return errors.New("username or password error") 59 | } 60 | } else if rs.Method != socks5.MethodNone { 61 | return errors.New("unsupported auth method") 62 | } 63 | return nil 64 | } 65 | 66 | func (c *SOCKS5Client) request(conn *net.TCPConn, r *socks5.Request) (*socks5.Reply, error) { 67 | if _, err := r.WriteTo(conn); err != nil { 68 | return nil, err 69 | } 70 | reply, err := socks5.NewReplyFrom(conn) 71 | if err != nil { 72 | return nil, err 73 | } 74 | return reply, nil 75 | } 76 | 77 | func (c *SOCKS5Client) DialTCP(raddr *AddrEx) (*net.TCPConn, error) { 78 | conn, err := net.DialTCP("tcp", nil, c.ServerTCPAddr) 79 | if err != nil { 80 | return nil, err 81 | } 82 | if err := conn.SetDeadline(time.Now().Add(c.NegTimeout)); err != nil { 83 | _ = conn.Close() 84 | return nil, err 85 | } 86 | err = c.negotiate(conn) 87 | if err != nil { 88 | _ = conn.Close() 89 | return nil, err 90 | } 91 | atyp, addr, port, err := addrExToSOCKS5Addr(raddr) 92 | if err != nil { 93 | _ = conn.Close() 94 | return nil, err 95 | } 96 | r := socks5.NewRequest(socks5.CmdConnect, atyp, addr, port) 97 | reply, err := c.request(conn, r) 98 | if err != nil { 99 | _ = conn.Close() 100 | return nil, err 101 | } 102 | if reply.Rep != socks5.RepSuccess { 103 | _ = conn.Close() 104 | return nil, fmt.Errorf("request failed: %d", reply.Rep) 105 | } 106 | // Negotiation succeed, disable timeout 107 | if err := conn.SetDeadline(time.Time{}); err != nil { 108 | _ = conn.Close() 109 | return nil, err 110 | } 111 | return conn, nil 112 | } 113 | 114 | func (c *SOCKS5Client) ListenUDP() (*socks5UDPConn, error) { 115 | conn, err := net.DialTCP("tcp", nil, c.ServerTCPAddr) 116 | if err != nil { 117 | return nil, err 118 | } 119 | if err := conn.SetDeadline(time.Now().Add(c.NegTimeout)); err != nil { 120 | _ = conn.Close() 121 | return nil, err 122 | } 123 | err = c.negotiate(conn) 124 | if err != nil { 125 | _ = conn.Close() 126 | return nil, err 127 | } 128 | r := socks5.NewRequest(socks5.CmdUDP, socks5.ATYPIPv4, nil, nil) 129 | reply, err := c.request(conn, r) 130 | if err != nil { 131 | _ = conn.Close() 132 | return nil, err 133 | } 134 | if reply.Rep != socks5.RepSuccess { 135 | _ = conn.Close() 136 | return nil, fmt.Errorf("request failed: %d", reply.Rep) 137 | } 138 | // Negotiation succeed, disable timeout 139 | if err := conn.SetDeadline(time.Time{}); err != nil { 140 | _ = conn.Close() 141 | return nil, err 142 | } 143 | udpRelayAddr, err := socks5AddrToUDPAddr(reply.Atyp, reply.BndAddr, reply.BndPort) 144 | if err != nil { 145 | _ = conn.Close() 146 | return nil, err 147 | } 148 | udpConn, err := net.DialUDP("udp", nil, udpRelayAddr) 149 | if err != nil { 150 | _ = conn.Close() 151 | return nil, err 152 | } 153 | sc := &socks5UDPConn{ 154 | tcpConn: conn, 155 | udpConn: udpConn, 156 | } 157 | go sc.hold() 158 | return sc, nil 159 | } 160 | 161 | type socks5UDPConn struct { 162 | tcpConn *net.TCPConn 163 | udpConn *net.UDPConn 164 | } 165 | 166 | func (c *socks5UDPConn) hold() { 167 | buf := make([]byte, 1024) 168 | for { 169 | _, err := c.tcpConn.Read(buf) 170 | if err != nil { 171 | break 172 | } 173 | } 174 | _ = c.tcpConn.Close() 175 | _ = c.udpConn.Close() 176 | } 177 | 178 | func (c *socks5UDPConn) ReadFromUDP(b []byte) (int, *net.UDPAddr, error) { 179 | n, err := c.udpConn.Read(b) 180 | if err != nil { 181 | return 0, nil, err 182 | } 183 | d, err := socks5.NewDatagramFromBytes(b[:n]) 184 | if err != nil { 185 | return 0, nil, err 186 | } 187 | addr, err := socks5AddrToUDPAddr(d.Atyp, d.DstAddr, d.DstPort) 188 | if err != nil { 189 | return 0, nil, err 190 | } 191 | n = copy(b, d.Data) 192 | return n, addr, nil 193 | } 194 | 195 | func (c *socks5UDPConn) WriteToUDP(b []byte, addr *AddrEx) (int, error) { 196 | atyp, dstAddr, dstPort, err := addrExToSOCKS5Addr(addr) 197 | if err != nil { 198 | return 0, err 199 | } 200 | d := socks5.NewDatagram(atyp, dstAddr, dstPort, b) 201 | _, err = c.udpConn.Write(d.Bytes()) 202 | if err != nil { 203 | return 0, err 204 | } 205 | return len(b), nil 206 | } 207 | 208 | func (c *socks5UDPConn) Close() error { 209 | _ = c.tcpConn.Close() 210 | _ = c.udpConn.Close() 211 | return nil 212 | } 213 | 214 | func socks5AddrToUDPAddr(atyp byte, addr []byte, port []byte) (*net.UDPAddr, error) { 215 | iPort := int(binary.BigEndian.Uint16(port)) 216 | switch atyp { 217 | case socks5.ATYPIPv4: 218 | if len(addr) != 4 { 219 | return nil, errors.New("invalid ipv4 address") 220 | } 221 | return &net.UDPAddr{ 222 | IP: addr, 223 | Port: iPort, 224 | }, nil 225 | case socks5.ATYPIPv6: 226 | if len(addr) != 16 { 227 | return nil, errors.New("invalid ipv6 address") 228 | } 229 | return &net.UDPAddr{ 230 | IP: addr, 231 | Port: iPort, 232 | }, nil 233 | case socks5.ATYPDomain: 234 | if len(addr) <= 1 { 235 | return nil, errors.New("invalid domain address") 236 | } 237 | ipAddr, err := net.ResolveIPAddr("ip", string(addr[1:])) 238 | if err != nil { 239 | return nil, err 240 | } 241 | return &net.UDPAddr{ 242 | IP: ipAddr.IP, 243 | Port: iPort, 244 | Zone: ipAddr.Zone, 245 | }, nil 246 | default: 247 | return nil, errors.New("unsupported address type") 248 | } 249 | } 250 | 251 | func addrExToSOCKS5Addr(addr *AddrEx) (byte, []byte, []byte, error) { 252 | sport := make([]byte, 2) 253 | binary.BigEndian.PutUint16(sport, uint16(addr.Port)) 254 | if len(addr.Domain) > 0 { 255 | return socks5.ATYPDomain, []byte(addr.Domain), sport, nil 256 | } else { 257 | var atyp byte 258 | var saddr []byte 259 | if ip4 := addr.IPAddr.IP.To4(); ip4 != nil { 260 | atyp = socks5.ATYPIPv4 261 | saddr = ip4 262 | } else if ip6 := addr.IPAddr.IP.To16(); ip6 != nil { 263 | atyp = socks5.ATYPIPv6 264 | saddr = ip6 265 | } else { 266 | return 0, nil, nil, errors.New("unsupported address type") 267 | } 268 | return atyp, saddr, sport, nil 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /pkg/core/frag_test.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func Test_fragUDPMessage(t *testing.T) { 9 | type args struct { 10 | m udpMessage 11 | maxSize int 12 | } 13 | tests := []struct { 14 | name string 15 | args args 16 | want []udpMessage 17 | }{ 18 | { 19 | "no frag", 20 | args{ 21 | udpMessage{ 22 | SessionID: 123, 23 | HostLen: 4, 24 | Host: "test", 25 | Port: 123, 26 | MsgID: 123, 27 | FragID: 0, 28 | FragCount: 1, 29 | DataLen: 5, 30 | Data: []byte("hello"), 31 | }, 32 | 100, 33 | }, 34 | []udpMessage{ 35 | { 36 | SessionID: 123, 37 | HostLen: 4, 38 | Host: "test", 39 | Port: 123, 40 | MsgID: 123, 41 | FragID: 0, 42 | FragCount: 1, 43 | DataLen: 5, 44 | Data: []byte("hello"), 45 | }, 46 | }, 47 | }, 48 | { 49 | "2 frags", 50 | args{ 51 | udpMessage{ 52 | SessionID: 123, 53 | HostLen: 4, 54 | Host: "test", 55 | Port: 123, 56 | MsgID: 123, 57 | FragID: 0, 58 | FragCount: 1, 59 | DataLen: 5, 60 | Data: []byte("hello"), 61 | }, 62 | 22, 63 | }, 64 | []udpMessage{ 65 | { 66 | SessionID: 123, 67 | HostLen: 4, 68 | Host: "test", 69 | Port: 123, 70 | MsgID: 123, 71 | FragID: 0, 72 | FragCount: 2, 73 | DataLen: 4, 74 | Data: []byte("hell"), 75 | }, 76 | { 77 | SessionID: 123, 78 | HostLen: 4, 79 | Host: "test", 80 | Port: 123, 81 | MsgID: 123, 82 | FragID: 1, 83 | FragCount: 2, 84 | DataLen: 1, 85 | Data: []byte("o"), 86 | }, 87 | }, 88 | }, 89 | { 90 | "4 frags", 91 | args{ 92 | udpMessage{ 93 | SessionID: 123, 94 | HostLen: 4, 95 | Host: "test", 96 | Port: 123, 97 | MsgID: 123, 98 | FragID: 0, 99 | FragCount: 1, 100 | DataLen: 20, 101 | Data: []byte("wow wow wow lol lmao"), 102 | }, 103 | 23, 104 | }, 105 | []udpMessage{ 106 | { 107 | SessionID: 123, 108 | HostLen: 4, 109 | Host: "test", 110 | Port: 123, 111 | MsgID: 123, 112 | FragID: 0, 113 | FragCount: 4, 114 | DataLen: 5, 115 | Data: []byte("wow w"), 116 | }, 117 | { 118 | SessionID: 123, 119 | HostLen: 4, 120 | Host: "test", 121 | Port: 123, 122 | MsgID: 123, 123 | FragID: 1, 124 | FragCount: 4, 125 | DataLen: 5, 126 | Data: []byte("ow wo"), 127 | }, 128 | { 129 | SessionID: 123, 130 | HostLen: 4, 131 | Host: "test", 132 | Port: 123, 133 | MsgID: 123, 134 | FragID: 2, 135 | FragCount: 4, 136 | DataLen: 5, 137 | Data: []byte("w lol"), 138 | }, 139 | { 140 | SessionID: 123, 141 | HostLen: 4, 142 | Host: "test", 143 | Port: 123, 144 | MsgID: 123, 145 | FragID: 3, 146 | FragCount: 4, 147 | DataLen: 5, 148 | Data: []byte(" lmao"), 149 | }, 150 | }, 151 | }, 152 | } 153 | for _, tt := range tests { 154 | t.Run(tt.name, func(t *testing.T) { 155 | if got := fragUDPMessage(tt.args.m, tt.args.maxSize); !reflect.DeepEqual(got, tt.want) { 156 | t.Errorf("fragUDPMessage() = %v, want %v", got, tt.want) 157 | } 158 | }) 159 | } 160 | } 161 | 162 | func Test_defragger_Feed(t *testing.T) { 163 | d := &defragger{} 164 | type args struct { 165 | m udpMessage 166 | } 167 | tests := []struct { 168 | name string 169 | args args 170 | want *udpMessage 171 | }{ 172 | { 173 | "no frag", 174 | args{ 175 | udpMessage{ 176 | SessionID: 123, 177 | HostLen: 4, 178 | Host: "test", 179 | Port: 123, 180 | MsgID: 123, 181 | FragID: 0, 182 | FragCount: 1, 183 | DataLen: 5, 184 | Data: []byte("hello"), 185 | }, 186 | }, 187 | &udpMessage{ 188 | SessionID: 123, 189 | HostLen: 4, 190 | Host: "test", 191 | Port: 123, 192 | MsgID: 123, 193 | FragID: 0, 194 | FragCount: 1, 195 | DataLen: 5, 196 | Data: []byte("hello"), 197 | }, 198 | }, 199 | { 200 | "frag 1 - 1/3", 201 | args{ 202 | udpMessage{ 203 | SessionID: 123, 204 | HostLen: 4, 205 | Host: "test", 206 | Port: 123, 207 | MsgID: 666, 208 | FragID: 0, 209 | FragCount: 3, 210 | DataLen: 5, 211 | Data: []byte("hello"), 212 | }, 213 | }, 214 | nil, 215 | }, 216 | { 217 | "frag 1 - 2/3", 218 | args{ 219 | udpMessage{ 220 | SessionID: 123, 221 | HostLen: 4, 222 | Host: "test", 223 | Port: 123, 224 | MsgID: 666, 225 | FragID: 1, 226 | FragCount: 3, 227 | DataLen: 8, 228 | Data: []byte(" shitty "), 229 | }, 230 | }, 231 | nil, 232 | }, 233 | { 234 | "frag 1 - 3/3", 235 | args{ 236 | udpMessage{ 237 | SessionID: 123, 238 | HostLen: 4, 239 | Host: "test", 240 | Port: 123, 241 | MsgID: 666, 242 | FragID: 2, 243 | FragCount: 3, 244 | DataLen: 7, 245 | Data: []byte("world!!"), 246 | }, 247 | }, 248 | &udpMessage{ 249 | SessionID: 123, 250 | HostLen: 4, 251 | Host: "test", 252 | Port: 123, 253 | MsgID: 666, 254 | FragID: 0, 255 | FragCount: 1, 256 | DataLen: 20, 257 | Data: []byte("hello shitty world!!"), 258 | }, 259 | }, 260 | { 261 | "frag 2 - 1/2", 262 | args{ 263 | udpMessage{ 264 | SessionID: 123, 265 | HostLen: 4, 266 | Host: "test", 267 | Port: 123, 268 | MsgID: 777, 269 | FragID: 0, 270 | FragCount: 2, 271 | DataLen: 5, 272 | Data: []byte("hello"), 273 | }, 274 | }, 275 | nil, 276 | }, 277 | { 278 | "frag 3 - 2/2", 279 | args{ 280 | udpMessage{ 281 | SessionID: 123, 282 | HostLen: 4, 283 | Host: "test", 284 | Port: 123, 285 | MsgID: 778, 286 | FragID: 1, 287 | FragCount: 2, 288 | DataLen: 5, 289 | Data: []byte(" moto"), 290 | }, 291 | }, 292 | nil, 293 | }, 294 | { 295 | "frag 2 - 2/2", 296 | args{ 297 | udpMessage{ 298 | SessionID: 123, 299 | HostLen: 4, 300 | Host: "test", 301 | Port: 123, 302 | MsgID: 777, 303 | FragID: 1, 304 | FragCount: 2, 305 | DataLen: 5, 306 | Data: []byte(" moto"), 307 | }, 308 | }, 309 | nil, 310 | }, 311 | { 312 | "frag 2 - 1/2 re", 313 | args{ 314 | udpMessage{ 315 | SessionID: 123, 316 | HostLen: 4, 317 | Host: "test", 318 | Port: 123, 319 | MsgID: 777, 320 | FragID: 0, 321 | FragCount: 2, 322 | DataLen: 5, 323 | Data: []byte("hello"), 324 | }, 325 | }, 326 | &udpMessage{ 327 | SessionID: 123, 328 | HostLen: 4, 329 | Host: "test", 330 | Port: 123, 331 | MsgID: 777, 332 | FragID: 0, 333 | FragCount: 1, 334 | DataLen: 10, 335 | Data: []byte("hello moto"), 336 | }, 337 | }, 338 | } 339 | for _, tt := range tests { 340 | t.Run(tt.name, func(t *testing.T) { 341 | if got := d.Feed(tt.args.m); !reflect.DeepEqual(got, tt.want) { 342 | t.Errorf("Feed() = %v, want %v", got, tt.want) 343 | } 344 | }) 345 | } 346 | } 347 | -------------------------------------------------------------------------------- /pkg/acl/entry.go: -------------------------------------------------------------------------------- 1 | package acl 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/oschwald/geoip2-golang" 11 | ) 12 | 13 | type ( 14 | Action byte 15 | Protocol byte 16 | ) 17 | 18 | const ( 19 | ActionDirect = Action(iota) 20 | ActionProxy 21 | ActionBlock 22 | ActionHijack 23 | ) 24 | 25 | const ( 26 | ProtocolAll = Protocol(iota) 27 | ProtocolTCP 28 | ProtocolUDP 29 | ) 30 | 31 | var protocolPortAliases = map[string]string{ 32 | "echo": "*/7", 33 | "ftp-data": "*/20", 34 | "ftp": "*/21", 35 | "ssh": "*/22", 36 | "telnet": "*/23", 37 | "domain": "*/53", 38 | "dns": "*/53", 39 | "http": "*/80", 40 | "sftp": "*/115", 41 | "ntp": "*/123", 42 | "https": "*/443", 43 | "quic": "udp/443", 44 | "socks": "*/1080", 45 | } 46 | 47 | type Entry struct { 48 | Action Action 49 | ActionArg string 50 | Matcher Matcher 51 | } 52 | 53 | type MatchRequest struct { 54 | IP net.IP 55 | Domain string 56 | 57 | Protocol Protocol 58 | Port uint16 59 | 60 | DB *geoip2.Reader 61 | } 62 | 63 | type Matcher interface { 64 | Match(MatchRequest) bool 65 | } 66 | 67 | type matcherBase struct { 68 | Protocol Protocol 69 | Port uint16 // 0 for all ports 70 | } 71 | 72 | func (m *matcherBase) MatchProtocolPort(p Protocol, port uint16) bool { 73 | return (m.Protocol == ProtocolAll || m.Protocol == p) && (m.Port == 0 || m.Port == port) 74 | } 75 | 76 | func parseProtocolPort(s string) (Protocol, uint16, error) { 77 | if protocolPortAliases[s] != "" { 78 | s = protocolPortAliases[s] 79 | } 80 | if len(s) == 0 || s == "*" { 81 | return ProtocolAll, 0, nil 82 | } 83 | parts := strings.Split(s, "/") 84 | if len(parts) != 2 { 85 | return ProtocolAll, 0, errors.New("invalid protocol/port syntax") 86 | } 87 | protocol := ProtocolAll 88 | switch parts[0] { 89 | case "tcp": 90 | protocol = ProtocolTCP 91 | case "udp": 92 | protocol = ProtocolUDP 93 | case "*": 94 | protocol = ProtocolAll 95 | default: 96 | return ProtocolAll, 0, errors.New("invalid protocol") 97 | } 98 | if parts[1] == "*" { 99 | return protocol, 0, nil 100 | } 101 | port, err := strconv.ParseUint(parts[1], 10, 16) 102 | if err != nil { 103 | return ProtocolAll, 0, errors.New("invalid port") 104 | } 105 | return protocol, uint16(port), nil 106 | } 107 | 108 | type netMatcher struct { 109 | matcherBase 110 | Net *net.IPNet 111 | } 112 | 113 | func (m *netMatcher) Match(r MatchRequest) bool { 114 | if r.IP == nil { 115 | return false 116 | } 117 | return m.Net.Contains(r.IP) && m.MatchProtocolPort(r.Protocol, r.Port) 118 | } 119 | 120 | type domainMatcher struct { 121 | matcherBase 122 | Domain string 123 | Suffix bool 124 | } 125 | 126 | func (m *domainMatcher) Match(r MatchRequest) bool { 127 | if len(r.Domain) == 0 { 128 | return false 129 | } 130 | domain := strings.ToLower(r.Domain) 131 | return (m.Domain == domain || (m.Suffix && strings.HasSuffix(domain, "."+m.Domain))) && 132 | m.MatchProtocolPort(r.Protocol, r.Port) 133 | } 134 | 135 | type countryMatcher struct { 136 | matcherBase 137 | Country string // ISO 3166-1 alpha-2 country code, upper case 138 | } 139 | 140 | func (m *countryMatcher) Match(r MatchRequest) bool { 141 | if r.IP == nil || r.DB == nil { 142 | return false 143 | } 144 | c, err := r.DB.Country(r.IP) 145 | if err != nil { 146 | return false 147 | } 148 | return c.Country.IsoCode == m.Country && m.MatchProtocolPort(r.Protocol, r.Port) 149 | } 150 | 151 | type allMatcher struct { 152 | matcherBase 153 | } 154 | 155 | func (m *allMatcher) Match(r MatchRequest) bool { 156 | return m.MatchProtocolPort(r.Protocol, r.Port) 157 | } 158 | 159 | func (e Entry) Match(r MatchRequest) bool { 160 | return e.Matcher.Match(r) 161 | } 162 | 163 | func ParseEntry(s string) (Entry, error) { 164 | fields := strings.Fields(s) 165 | if len(fields) < 2 { 166 | return Entry{}, fmt.Errorf("expected at least 2 fields, got %d", len(fields)) 167 | } 168 | e := Entry{} 169 | action := fields[0] 170 | conds := fields[1:] 171 | switch strings.ToLower(action) { 172 | case "direct": 173 | e.Action = ActionDirect 174 | case "proxy": 175 | e.Action = ActionProxy 176 | case "block": 177 | e.Action = ActionBlock 178 | case "hijack": 179 | if len(conds) < 2 { 180 | return Entry{}, fmt.Errorf("hijack requires at least 3 fields, got %d", len(fields)) 181 | } 182 | e.Action = ActionHijack 183 | e.ActionArg = conds[len(conds)-1] 184 | conds = conds[:len(conds)-1] 185 | default: 186 | return Entry{}, fmt.Errorf("invalid action %s", fields[0]) 187 | } 188 | m, err := condsToMatcher(conds) 189 | if err != nil { 190 | return Entry{}, err 191 | } 192 | e.Matcher = m 193 | return e, nil 194 | } 195 | 196 | func condsToMatcher(conds []string) (Matcher, error) { 197 | if len(conds) < 1 { 198 | return nil, errors.New("no condition specified") 199 | } 200 | typ, args := conds[0], conds[1:] 201 | switch strings.ToLower(typ) { 202 | case "domain": 203 | // domain 204 | if len(args) == 0 || len(args) > 2 { 205 | return nil, fmt.Errorf("invalid number of arguments for domain: %d, expected 1 or 2", len(args)) 206 | } 207 | mb := matcherBase{} 208 | if len(args) == 2 { 209 | protocol, port, err := parseProtocolPort(args[1]) 210 | if err != nil { 211 | return nil, err 212 | } 213 | mb.Protocol = protocol 214 | mb.Port = port 215 | } 216 | return &domainMatcher{ 217 | matcherBase: mb, 218 | Domain: args[0], 219 | Suffix: false, 220 | }, nil 221 | case "domain-suffix": 222 | // domain-suffix 223 | if len(args) == 0 || len(args) > 2 { 224 | return nil, fmt.Errorf("invalid number of arguments for domain-suffix: %d, expected 1 or 2", len(args)) 225 | } 226 | mb := matcherBase{} 227 | if len(args) == 2 { 228 | protocol, port, err := parseProtocolPort(args[1]) 229 | if err != nil { 230 | return nil, err 231 | } 232 | mb.Protocol = protocol 233 | mb.Port = port 234 | } 235 | return &domainMatcher{ 236 | matcherBase: mb, 237 | Domain: args[0], 238 | Suffix: true, 239 | }, nil 240 | case "cidr": 241 | // cidr 242 | if len(args) == 0 || len(args) > 2 { 243 | return nil, fmt.Errorf("invalid number of arguments for cidr: %d, expected 1 or 2", len(args)) 244 | } 245 | mb := matcherBase{} 246 | if len(args) == 2 { 247 | protocol, port, err := parseProtocolPort(args[1]) 248 | if err != nil { 249 | return nil, err 250 | } 251 | mb.Protocol = protocol 252 | mb.Port = port 253 | } 254 | _, ipNet, err := net.ParseCIDR(args[0]) 255 | if err != nil { 256 | return nil, err 257 | } 258 | return &netMatcher{ 259 | matcherBase: mb, 260 | Net: ipNet, 261 | }, nil 262 | case "ip": 263 | // ip 264 | if len(args) == 0 || len(args) > 2 { 265 | return nil, fmt.Errorf("invalid number of arguments for ip: %d, expected 1 or 2", len(args)) 266 | } 267 | mb := matcherBase{} 268 | if len(args) == 2 { 269 | protocol, port, err := parseProtocolPort(args[1]) 270 | if err != nil { 271 | return nil, err 272 | } 273 | mb.Protocol = protocol 274 | mb.Port = port 275 | } 276 | ip := net.ParseIP(args[0]) 277 | if ip == nil { 278 | return nil, fmt.Errorf("invalid ip: %s", args[0]) 279 | } 280 | var ipNet *net.IPNet 281 | if ip.To4() != nil { 282 | ipNet = &net.IPNet{ 283 | IP: ip, 284 | Mask: net.CIDRMask(32, 32), 285 | } 286 | } else { 287 | ipNet = &net.IPNet{ 288 | IP: ip, 289 | Mask: net.CIDRMask(128, 128), 290 | } 291 | } 292 | return &netMatcher{ 293 | matcherBase: mb, 294 | Net: ipNet, 295 | }, nil 296 | case "country": 297 | // country 298 | if len(args) == 0 || len(args) > 2 { 299 | return nil, fmt.Errorf("invalid number of arguments for country: %d, expected 1 or 2", len(args)) 300 | } 301 | mb := matcherBase{} 302 | if len(args) == 2 { 303 | protocol, port, err := parseProtocolPort(args[1]) 304 | if err != nil { 305 | return nil, err 306 | } 307 | mb.Protocol = protocol 308 | mb.Port = port 309 | } 310 | return &countryMatcher{ 311 | matcherBase: mb, 312 | Country: strings.ToUpper(args[0]), 313 | }, nil 314 | case "all": 315 | // all 316 | if len(args) > 1 { 317 | return nil, fmt.Errorf("invalid number of arguments for all: %d, expected 0 or 1", len(args)) 318 | } 319 | mb := matcherBase{} 320 | if len(args) == 1 { 321 | protocol, port, err := parseProtocolPort(args[0]) 322 | if err != nil { 323 | return nil, err 324 | } 325 | mb.Protocol = protocol 326 | mb.Port = port 327 | } 328 | return &allMatcher{ 329 | matcherBase: mb, 330 | }, nil 331 | default: 332 | return nil, fmt.Errorf("invalid condition type: %s", typ) 333 | } 334 | } 335 | -------------------------------------------------------------------------------- /cmd/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "regexp" 7 | "strconv" 8 | "time" 9 | 10 | "github.com/sirupsen/logrus" 11 | "github.com/yosuke-furukawa/json5/encoding/json5" 12 | ) 13 | 14 | const ( 15 | mbpsToBps = 125000 16 | minSpeedBPS = 16384 17 | 18 | DefaultStreamReceiveWindow = 15728640 // 15 MB/s 19 | DefaultConnectionReceiveWindow = 67108864 // 64 MB/s 20 | DefaultMaxIncomingStreams = 1024 21 | 22 | DefaultALPN = "hysteria" 23 | 24 | DefaultMMDBFilename = "GeoLite2-Country.mmdb" 25 | 26 | KeepAlivePeriod = 10 * time.Second 27 | ) 28 | 29 | var rateStringRegexp = regexp.MustCompile(`^(\d+)\s*([KMGT]?)([Bb])ps$`) 30 | 31 | type serverConfig struct { 32 | Listen string `json:"listen"` 33 | Protocol string `json:"protocol"` 34 | ACME struct { 35 | Domains []string `json:"domains"` 36 | Email string `json:"email"` 37 | DisableHTTPChallenge bool `json:"disable_http"` 38 | DisableTLSALPNChallenge bool `json:"disable_tlsalpn"` 39 | AltHTTPPort int `json:"alt_http_port"` 40 | AltTLSALPNPort int `json:"alt_tlsalpn_port"` 41 | } `json:"acme"` 42 | CertFile string `json:"cert"` 43 | KeyFile string `json:"key"` 44 | // Optional below 45 | Up string `json:"up"` 46 | UpMbps int `json:"up_mbps"` 47 | Down string `json:"down"` 48 | DownMbps int `json:"down_mbps"` 49 | DisableUDP bool `json:"disable_udp"` 50 | ACL string `json:"acl"` 51 | MMDB string `json:"mmdb"` 52 | Obfs string `json:"obfs"` 53 | Auth struct { 54 | Mode string `json:"mode"` 55 | Config json5.RawMessage `json:"config"` 56 | } `json:"auth"` 57 | ALPN string `json:"alpn"` 58 | PrometheusListen string `json:"prometheus_listen"` 59 | ReceiveWindowConn uint64 `json:"recv_window_conn"` 60 | ReceiveWindowClient uint64 `json:"recv_window_client"` 61 | MaxConnClient int `json:"max_conn_client"` 62 | DisableMTUDiscovery bool `json:"disable_mtu_discovery"` 63 | Resolver string `json:"resolver"` 64 | ResolvePreference string `json:"resolve_preference"` 65 | SOCKS5Outbound struct { 66 | Server string `json:"server"` 67 | User string `json:"user"` 68 | Password string `json:"password"` 69 | } `json:"socks5_outbound"` 70 | BindOutbound struct { 71 | Address string `json:"address"` 72 | Device string `json:"device"` 73 | } `json:"bind_outbound"` 74 | } 75 | 76 | func (c *serverConfig) Speed() (uint64, uint64, error) { 77 | var up, down uint64 78 | if len(c.Up) > 0 { 79 | up = stringToBps(c.Up) 80 | if up == 0 { 81 | return 0, 0, errors.New("invalid speed format") 82 | } 83 | } else { 84 | up = uint64(c.UpMbps) * mbpsToBps 85 | } 86 | if len(c.Down) > 0 { 87 | down = stringToBps(c.Down) 88 | if down == 0 { 89 | return 0, 0, errors.New("invalid speed format") 90 | } 91 | } else { 92 | down = uint64(c.DownMbps) * mbpsToBps 93 | } 94 | return up, down, nil 95 | } 96 | 97 | func (c *serverConfig) Check() error { 98 | if len(c.Listen) == 0 { 99 | return errors.New("no listen address") 100 | } 101 | if len(c.ACME.Domains) == 0 && (len(c.CertFile) == 0 || len(c.KeyFile) == 0) { 102 | return errors.New("ACME domain or TLS cert not provided") 103 | } 104 | if up, down, err := c.Speed(); err != nil || (up != 0 && up < minSpeedBPS) || (down != 0 && down < minSpeedBPS) { 105 | return errors.New("invalid speed") 106 | } 107 | if (c.ReceiveWindowConn != 0 && c.ReceiveWindowConn < 65536) || 108 | (c.ReceiveWindowClient != 0 && c.ReceiveWindowClient < 65536) { 109 | return errors.New("invalid receive window size") 110 | } 111 | if c.MaxConnClient < 0 { 112 | return errors.New("invalid max connections per client") 113 | } 114 | return nil 115 | } 116 | 117 | func (c *serverConfig) String() string { 118 | return fmt.Sprintf("%+v", *c) 119 | } 120 | 121 | type Relay struct { 122 | Listen string `json:"listen"` 123 | Remote string `json:"remote"` 124 | Timeout int `json:"timeout"` 125 | } 126 | 127 | func (r *Relay) Check() error { 128 | if len(r.Listen) == 0 { 129 | return errors.New("no relay listen address") 130 | } 131 | if len(r.Remote) == 0 { 132 | return errors.New("no relay remote address") 133 | } 134 | if r.Timeout != 0 && r.Timeout <= 4 { 135 | return errors.New("invalid relay timeout") 136 | } 137 | return nil 138 | } 139 | 140 | type clientConfig struct { 141 | Server string `json:"server"` 142 | Protocol string `json:"protocol"` 143 | Up string `json:"up"` 144 | UpMbps int `json:"up_mbps"` 145 | Down string `json:"down"` 146 | DownMbps int `json:"down_mbps"` 147 | Retry int `json:"retry"` 148 | RetryInterval int `json:"retry_interval"` 149 | // Optional below 150 | SOCKS5 struct { 151 | Listen string `json:"listen"` 152 | Timeout int `json:"timeout"` 153 | DisableUDP bool `json:"disable_udp"` 154 | User string `json:"user"` 155 | Password string `json:"password"` 156 | } `json:"socks5"` 157 | HTTP struct { 158 | Listen string `json:"listen"` 159 | Timeout int `json:"timeout"` 160 | User string `json:"user"` 161 | Password string `json:"password"` 162 | Cert string `json:"cert"` 163 | Key string `json:"key"` 164 | } `json:"http"` 165 | TUN struct { 166 | Name string `json:"name"` 167 | Timeout int `json:"timeout"` 168 | MTU uint32 `json:"mtu"` 169 | TCPSendBufferSize string `json:"tcp_sndbuf"` 170 | TCPReceiveBufferSize string `json:"tcp_rcvbuf"` 171 | TCPModerateReceiveBuffer bool `json:"tcp_autotuning"` 172 | } `json:"tun"` 173 | TCPRelays []Relay `json:"relay_tcps"` 174 | TCPRelay Relay `json:"relay_tcp"` // deprecated, but we still support it for backward compatibility 175 | UDPRelays []Relay `json:"relay_udps"` 176 | UDPRelay Relay `json:"relay_udp"` // deprecated, but we still support it for backward compatibility 177 | TCPTProxy struct { 178 | Listen string `json:"listen"` 179 | Timeout int `json:"timeout"` 180 | } `json:"tproxy_tcp"` 181 | UDPTProxy struct { 182 | Listen string `json:"listen"` 183 | Timeout int `json:"timeout"` 184 | } `json:"tproxy_udp"` 185 | TCPRedirect struct { 186 | Listen string `json:"listen"` 187 | Timeout int `json:"timeout"` 188 | } `json:"redirect_tcp"` 189 | ACL string `json:"acl"` 190 | MMDB string `json:"mmdb"` 191 | Obfs string `json:"obfs"` 192 | Auth []byte `json:"auth"` 193 | AuthString string `json:"auth_str"` 194 | ALPN string `json:"alpn"` 195 | ServerName string `json:"server_name"` 196 | Insecure bool `json:"insecure"` 197 | CustomCA string `json:"ca"` 198 | ReceiveWindowConn uint64 `json:"recv_window_conn"` 199 | ReceiveWindow uint64 `json:"recv_window"` 200 | DisableMTUDiscovery bool `json:"disable_mtu_discovery"` 201 | Resolver string `json:"resolver"` 202 | ResolvePreference string `json:"resolve_preference"` 203 | } 204 | 205 | func (c *clientConfig) Speed() (uint64, uint64, error) { 206 | var up, down uint64 207 | if len(c.Up) > 0 { 208 | up = stringToBps(c.Up) 209 | if up == 0 { 210 | return 0, 0, errors.New("invalid speed format") 211 | } 212 | } else { 213 | up = uint64(c.UpMbps) * mbpsToBps 214 | } 215 | if len(c.Down) > 0 { 216 | down = stringToBps(c.Down) 217 | if down == 0 { 218 | return 0, 0, errors.New("invalid speed format") 219 | } 220 | } else { 221 | down = uint64(c.DownMbps) * mbpsToBps 222 | } 223 | return up, down, nil 224 | } 225 | 226 | func (c *clientConfig) Check() error { 227 | if len(c.SOCKS5.Listen) == 0 && len(c.HTTP.Listen) == 0 && len(c.TUN.Name) == 0 && 228 | len(c.TCPRelay.Listen) == 0 && len(c.UDPRelay.Listen) == 0 && 229 | len(c.TCPRelays) == 0 && len(c.UDPRelays) == 0 && 230 | len(c.TCPTProxy.Listen) == 0 && len(c.UDPTProxy.Listen) == 0 && 231 | len(c.TCPRedirect.Listen) == 0 { 232 | return errors.New("please enable at least one mode") 233 | } 234 | if c.SOCKS5.Timeout != 0 && c.SOCKS5.Timeout <= 4 { 235 | return errors.New("invalid SOCKS5 timeout") 236 | } 237 | if c.HTTP.Timeout != 0 && c.HTTP.Timeout <= 4 { 238 | return errors.New("invalid HTTP timeout") 239 | } 240 | if c.TUN.Timeout != 0 && c.TUN.Timeout < 4 { 241 | return errors.New("invalid TUN timeout") 242 | } 243 | if len(c.TCPRelay.Listen) > 0 && len(c.TCPRelay.Remote) == 0 { 244 | return errors.New("no TCP relay remote address") 245 | } 246 | if len(c.UDPRelay.Listen) > 0 && len(c.UDPRelay.Remote) == 0 { 247 | return errors.New("no UDP relay remote address") 248 | } 249 | if c.TCPRelay.Timeout != 0 && c.TCPRelay.Timeout <= 4 { 250 | return errors.New("invalid TCP relay timeout") 251 | } 252 | if c.UDPRelay.Timeout != 0 && c.UDPRelay.Timeout <= 4 { 253 | return errors.New("invalid UDP relay timeout") 254 | } 255 | for _, r := range c.TCPRelays { 256 | if err := r.Check(); err != nil { 257 | return err 258 | } 259 | } 260 | for _, r := range c.UDPRelays { 261 | if err := r.Check(); err != nil { 262 | return err 263 | } 264 | } 265 | if c.TCPTProxy.Timeout != 0 && c.TCPTProxy.Timeout <= 4 { 266 | return errors.New("invalid TCP TProxy timeout") 267 | } 268 | if c.UDPTProxy.Timeout != 0 && c.UDPTProxy.Timeout <= 4 { 269 | return errors.New("invalid UDP TProxy timeout") 270 | } 271 | if c.TCPRedirect.Timeout != 0 && c.TCPRedirect.Timeout <= 4 { 272 | return errors.New("invalid TCP Redirect timeout") 273 | } 274 | if len(c.Server) == 0 { 275 | return errors.New("no server address") 276 | } 277 | if up, down, err := c.Speed(); err != nil || up < minSpeedBPS || down < minSpeedBPS { 278 | return errors.New("invalid speed") 279 | } 280 | if (c.ReceiveWindowConn != 0 && c.ReceiveWindowConn < 65536) || 281 | (c.ReceiveWindow != 0 && c.ReceiveWindow < 65536) { 282 | return errors.New("invalid receive window size") 283 | } 284 | if len(c.TCPRelay.Listen) > 0 { 285 | logrus.Warn("'relay_tcp' is deprecated, please use 'relay_tcps' instead") 286 | } 287 | if len(c.UDPRelay.Listen) > 0 { 288 | logrus.Warn("config 'relay_udp' is deprecated, please use 'relay_udps' instead") 289 | } 290 | return nil 291 | } 292 | 293 | func (c *clientConfig) String() string { 294 | return fmt.Sprintf("%+v", *c) 295 | } 296 | 297 | func stringToBps(s string) uint64 { 298 | if s == "" { 299 | return 0 300 | } 301 | m := rateStringRegexp.FindStringSubmatch(s) 302 | if m == nil { 303 | return 0 304 | } 305 | var n uint64 306 | switch m[2] { 307 | case "K": 308 | n = 1 << 10 309 | case "M": 310 | n = 1 << 20 311 | case "G": 312 | n = 1 << 30 313 | case "T": 314 | n = 1 << 40 315 | default: 316 | n = 1 317 | } 318 | v, _ := strconv.ParseUint(m[1], 10, 64) 319 | n = v * n 320 | if m[3] == "b" { 321 | // Bits, need to convert to bytes 322 | n = n >> 3 323 | } 324 | return n 325 | } 326 | -------------------------------------------------------------------------------- /pkg/core/client.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/tls" 7 | "errors" 8 | "fmt" 9 | "math/rand" 10 | "net" 11 | "strconv" 12 | "sync" 13 | "time" 14 | 15 | "github.com/HyNetwork/hysteria/pkg/obfs" 16 | "github.com/HyNetwork/hysteria/pkg/pmtud_fix" 17 | "github.com/HyNetwork/hysteria/pkg/transport" 18 | "github.com/HyNetwork/hysteria/pkg/utils" 19 | "github.com/lucas-clemente/quic-go" 20 | "github.com/lucas-clemente/quic-go/congestion" 21 | "github.com/lunixbochs/struc" 22 | ) 23 | 24 | var ErrClosed = errors.New("closed") 25 | 26 | type CongestionFactory func(refBPS uint64) congestion.CongestionControl 27 | 28 | type Client struct { 29 | transport *transport.ClientTransport 30 | serverAddr string 31 | protocol string 32 | sendBPS, recvBPS uint64 33 | auth []byte 34 | congestionFactory CongestionFactory 35 | obfuscator obfs.Obfuscator 36 | 37 | tlsConfig *tls.Config 38 | quicConfig *quic.Config 39 | 40 | quicSession quic.Connection 41 | reconnectMutex sync.Mutex 42 | closed bool 43 | 44 | udpSessionMutex sync.RWMutex 45 | udpSessionMap map[uint32]chan *udpMessage 46 | udpDefragger defragger 47 | } 48 | 49 | func NewClient(serverAddr string, protocol string, auth []byte, tlsConfig *tls.Config, quicConfig *quic.Config, 50 | transport *transport.ClientTransport, sendBPS uint64, recvBPS uint64, congestionFactory CongestionFactory, 51 | obfuscator obfs.Obfuscator, 52 | ) (*Client, error) { 53 | quicConfig.DisablePathMTUDiscovery = quicConfig.DisablePathMTUDiscovery || pmtud_fix.DisablePathMTUDiscovery 54 | c := &Client{ 55 | transport: transport, 56 | serverAddr: serverAddr, 57 | protocol: protocol, 58 | sendBPS: sendBPS, 59 | recvBPS: recvBPS, 60 | auth: auth, 61 | congestionFactory: congestionFactory, 62 | obfuscator: obfuscator, 63 | tlsConfig: tlsConfig, 64 | quicConfig: quicConfig, 65 | } 66 | if err := c.connectToServer(); err != nil { 67 | return nil, err 68 | } 69 | return c, nil 70 | } 71 | 72 | func (c *Client) connectToServer() error { 73 | qs, err := c.transport.QUICDial(c.protocol, c.serverAddr, c.tlsConfig, c.quicConfig, c.obfuscator) 74 | if err != nil { 75 | return err 76 | } 77 | // Control stream 78 | ctx, ctxCancel := context.WithTimeout(context.Background(), protocolTimeout) 79 | stream, err := qs.OpenStreamSync(ctx) 80 | ctxCancel() 81 | if err != nil { 82 | _ = qs.CloseWithError(closeErrorCodeProtocol, "protocol error") 83 | return err 84 | } 85 | ok, msg, err := c.handleControlStream(qs, stream) 86 | if err != nil { 87 | _ = qs.CloseWithError(closeErrorCodeProtocol, "protocol error") 88 | return err 89 | } 90 | if !ok { 91 | _ = qs.CloseWithError(closeErrorCodeAuth, "auth error") 92 | return fmt.Errorf("auth error: %s", msg) 93 | } 94 | // All good 95 | c.udpSessionMap = make(map[uint32]chan *udpMessage) 96 | go c.handleMessage(qs) 97 | c.quicSession = qs 98 | return nil 99 | } 100 | 101 | func (c *Client) handleControlStream(qs quic.Connection, stream quic.Stream) (bool, string, error) { 102 | // Send protocol version 103 | _, err := stream.Write([]byte{protocolVersion}) 104 | if err != nil { 105 | return false, "", err 106 | } 107 | // Send client hello 108 | err = struc.Pack(stream, &clientHello{ 109 | Rate: transmissionRate{ 110 | SendBPS: c.sendBPS, 111 | RecvBPS: c.recvBPS, 112 | }, 113 | Auth: c.auth, 114 | }) 115 | if err != nil { 116 | return false, "", err 117 | } 118 | // Receive server hello 119 | var sh serverHello 120 | err = struc.Unpack(stream, &sh) 121 | if err != nil { 122 | return false, "", err 123 | } 124 | // Set the congestion accordingly 125 | if sh.OK && c.congestionFactory != nil { 126 | qs.SetCongestionControl(c.congestionFactory(sh.Rate.RecvBPS)) 127 | } 128 | return sh.OK, sh.Message, nil 129 | } 130 | 131 | func (c *Client) handleMessage(qs quic.Connection) { 132 | for { 133 | msg, err := qs.ReceiveMessage() 134 | if err != nil { 135 | break 136 | } 137 | var udpMsg udpMessage 138 | err = struc.Unpack(bytes.NewBuffer(msg), &udpMsg) 139 | if err != nil { 140 | continue 141 | } 142 | dfMsg := c.udpDefragger.Feed(udpMsg) 143 | if dfMsg == nil { 144 | continue 145 | } 146 | c.udpSessionMutex.RLock() 147 | ch, ok := c.udpSessionMap[dfMsg.SessionID] 148 | if ok { 149 | select { 150 | case ch <- dfMsg: 151 | // OK 152 | default: 153 | // Silently drop the message when the channel is full 154 | } 155 | } 156 | c.udpSessionMutex.RUnlock() 157 | } 158 | } 159 | 160 | func (c *Client) openStreamWithReconnect() (quic.Connection, quic.Stream, error) { 161 | c.reconnectMutex.Lock() 162 | defer c.reconnectMutex.Unlock() 163 | if c.closed { 164 | return nil, nil, ErrClosed 165 | } 166 | stream, err := c.quicSession.OpenStream() 167 | if err == nil { 168 | // All good 169 | return c.quicSession, &wrappedQUICStream{stream}, nil 170 | } 171 | // Something is wrong 172 | if nErr, ok := err.(net.Error); ok && nErr.Temporary() { 173 | // Temporary error, just return 174 | return nil, nil, err 175 | } 176 | // Permanent error, need to reconnect 177 | if err := c.connectToServer(); err != nil { 178 | // Still error, oops 179 | return nil, nil, err 180 | } 181 | // We are not going to try again even if it still fails the second time 182 | stream, err = c.quicSession.OpenStream() 183 | return c.quicSession, &wrappedQUICStream{stream}, err 184 | } 185 | 186 | func (c *Client) DialTCP(addr string) (net.Conn, error) { 187 | host, port, err := utils.SplitHostPort(addr) 188 | if err != nil { 189 | return nil, err 190 | } 191 | session, stream, err := c.openStreamWithReconnect() 192 | if err != nil { 193 | return nil, err 194 | } 195 | // Send request 196 | err = struc.Pack(stream, &clientRequest{ 197 | UDP: false, 198 | Host: host, 199 | Port: port, 200 | }) 201 | if err != nil { 202 | _ = stream.Close() 203 | return nil, err 204 | } 205 | // Read response 206 | var sr serverResponse 207 | err = struc.Unpack(stream, &sr) 208 | if err != nil { 209 | _ = stream.Close() 210 | return nil, err 211 | } 212 | if !sr.OK { 213 | _ = stream.Close() 214 | return nil, fmt.Errorf("connection rejected: %s", sr.Message) 215 | } 216 | return &quicConn{ 217 | Orig: stream, 218 | PseudoLocalAddr: session.LocalAddr(), 219 | PseudoRemoteAddr: session.RemoteAddr(), 220 | }, nil 221 | } 222 | 223 | func (c *Client) DialUDP() (UDPConn, error) { 224 | session, stream, err := c.openStreamWithReconnect() 225 | if err != nil { 226 | return nil, err 227 | } 228 | // Send request 229 | err = struc.Pack(stream, &clientRequest{ 230 | UDP: true, 231 | }) 232 | if err != nil { 233 | _ = stream.Close() 234 | return nil, err 235 | } 236 | // Read response 237 | var sr serverResponse 238 | err = struc.Unpack(stream, &sr) 239 | if err != nil { 240 | _ = stream.Close() 241 | return nil, err 242 | } 243 | if !sr.OK { 244 | _ = stream.Close() 245 | return nil, fmt.Errorf("connection rejected: %s", sr.Message) 246 | } 247 | 248 | // Create a session in the map 249 | c.udpSessionMutex.Lock() 250 | nCh := make(chan *udpMessage, 1024) 251 | // Store the current session map for CloseFunc below 252 | // to ensures that we are adding and removing sessions on the same map, 253 | // as reconnecting will reassign the map 254 | sessionMap := c.udpSessionMap 255 | sessionMap[sr.UDPSessionID] = nCh 256 | c.udpSessionMutex.Unlock() 257 | 258 | pktConn := &quicPktConn{ 259 | Session: session, 260 | Stream: stream, 261 | CloseFunc: func() { 262 | c.udpSessionMutex.Lock() 263 | if ch, ok := sessionMap[sr.UDPSessionID]; ok { 264 | close(ch) 265 | delete(sessionMap, sr.UDPSessionID) 266 | } 267 | c.udpSessionMutex.Unlock() 268 | }, 269 | UDPSessionID: sr.UDPSessionID, 270 | MsgCh: nCh, 271 | } 272 | go pktConn.Hold() 273 | return pktConn, nil 274 | } 275 | 276 | func (c *Client) Close() error { 277 | c.reconnectMutex.Lock() 278 | defer c.reconnectMutex.Unlock() 279 | err := c.quicSession.CloseWithError(closeErrorCodeGeneric, "") 280 | c.closed = true 281 | return err 282 | } 283 | 284 | type quicConn struct { 285 | Orig quic.Stream 286 | PseudoLocalAddr net.Addr 287 | PseudoRemoteAddr net.Addr 288 | } 289 | 290 | func (w *quicConn) Read(b []byte) (n int, err error) { 291 | return w.Orig.Read(b) 292 | } 293 | 294 | func (w *quicConn) Write(b []byte) (n int, err error) { 295 | return w.Orig.Write(b) 296 | } 297 | 298 | func (w *quicConn) Close() error { 299 | return w.Orig.Close() 300 | } 301 | 302 | func (w *quicConn) LocalAddr() net.Addr { 303 | return w.PseudoLocalAddr 304 | } 305 | 306 | func (w *quicConn) RemoteAddr() net.Addr { 307 | return w.PseudoRemoteAddr 308 | } 309 | 310 | func (w *quicConn) SetDeadline(t time.Time) error { 311 | return w.Orig.SetDeadline(t) 312 | } 313 | 314 | func (w *quicConn) SetReadDeadline(t time.Time) error { 315 | return w.Orig.SetReadDeadline(t) 316 | } 317 | 318 | func (w *quicConn) SetWriteDeadline(t time.Time) error { 319 | return w.Orig.SetWriteDeadline(t) 320 | } 321 | 322 | type UDPConn interface { 323 | ReadFrom() ([]byte, string, error) 324 | WriteTo([]byte, string) error 325 | Close() error 326 | } 327 | 328 | type quicPktConn struct { 329 | Session quic.Connection 330 | Stream quic.Stream 331 | CloseFunc func() 332 | UDPSessionID uint32 333 | MsgCh <-chan *udpMessage 334 | } 335 | 336 | func (c *quicPktConn) Hold() { 337 | // Hold the stream until it's closed 338 | buf := make([]byte, 1024) 339 | for { 340 | _, err := c.Stream.Read(buf) 341 | if err != nil { 342 | break 343 | } 344 | } 345 | _ = c.Close() 346 | } 347 | 348 | func (c *quicPktConn) ReadFrom() ([]byte, string, error) { 349 | msg := <-c.MsgCh 350 | if msg == nil { 351 | // Closed 352 | return nil, "", ErrClosed 353 | } 354 | return msg.Data, net.JoinHostPort(msg.Host, strconv.Itoa(int(msg.Port))), nil 355 | } 356 | 357 | func (c *quicPktConn) WriteTo(p []byte, addr string) error { 358 | host, port, err := utils.SplitHostPort(addr) 359 | if err != nil { 360 | return err 361 | } 362 | msg := udpMessage{ 363 | SessionID: c.UDPSessionID, 364 | Host: host, 365 | Port: port, 366 | FragCount: 1, 367 | Data: p, 368 | } 369 | // try no frag first 370 | var msgBuf bytes.Buffer 371 | _ = struc.Pack(&msgBuf, &msg) 372 | err = c.Session.SendMessage(msgBuf.Bytes()) 373 | if err != nil { 374 | if errSize, ok := err.(quic.ErrMessageToLarge); ok { 375 | // need to frag 376 | msg.MsgID = uint16(rand.Intn(0xFFFF)) + 1 // msgID must be > 0 when fragCount > 1 377 | fragMsgs := fragUDPMessage(msg, int(errSize)) 378 | for _, fragMsg := range fragMsgs { 379 | msgBuf.Reset() 380 | _ = struc.Pack(&msgBuf, &fragMsg) 381 | err = c.Session.SendMessage(msgBuf.Bytes()) 382 | if err != nil { 383 | return err 384 | } 385 | } 386 | return nil 387 | } else { 388 | // some other error 389 | return err 390 | } 391 | } else { 392 | return nil 393 | } 394 | } 395 | 396 | func (c *quicPktConn) Close() error { 397 | c.CloseFunc() 398 | return c.Stream.Close() 399 | } 400 | -------------------------------------------------------------------------------- /pkg/core/server_client.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/base64" 7 | "math/rand" 8 | "net" 9 | "strconv" 10 | "sync" 11 | 12 | "github.com/HyNetwork/hysteria/pkg/acl" 13 | "github.com/HyNetwork/hysteria/pkg/transport" 14 | "github.com/HyNetwork/hysteria/pkg/utils" 15 | "github.com/lucas-clemente/quic-go" 16 | "github.com/lunixbochs/struc" 17 | "github.com/prometheus/client_golang/prometheus" 18 | ) 19 | 20 | const udpBufferSize = 65535 21 | 22 | type serverClient struct { 23 | V2 bool 24 | CS quic.Connection 25 | Transport *transport.ServerTransport 26 | Auth []byte 27 | ClientAddr net.Addr 28 | DisableUDP bool 29 | ACLEngine *acl.Engine 30 | CTCPRequestFunc TCPRequestFunc 31 | CTCPErrorFunc TCPErrorFunc 32 | CUDPRequestFunc UDPRequestFunc 33 | CUDPErrorFunc UDPErrorFunc 34 | 35 | UpCounter, DownCounter prometheus.Counter 36 | ConnGauge prometheus.Gauge 37 | 38 | udpSessionMutex sync.RWMutex 39 | udpSessionMap map[uint32]transport.PUDPConn 40 | nextUDPSessionID uint32 41 | udpDefragger defragger 42 | } 43 | 44 | func newServerClient(v2 bool, cs quic.Connection, tr *transport.ServerTransport, auth []byte, disableUDP bool, ACLEngine *acl.Engine, 45 | CTCPRequestFunc TCPRequestFunc, CTCPErrorFunc TCPErrorFunc, 46 | CUDPRequestFunc UDPRequestFunc, CUDPErrorFunc UDPErrorFunc, 47 | UpCounterVec, DownCounterVec *prometheus.CounterVec, 48 | ConnGaugeVec *prometheus.GaugeVec, 49 | ) *serverClient { 50 | sc := &serverClient{ 51 | V2: v2, 52 | CS: cs, 53 | Transport: tr, 54 | Auth: auth, 55 | ClientAddr: cs.RemoteAddr(), 56 | DisableUDP: disableUDP, 57 | ACLEngine: ACLEngine, 58 | CTCPRequestFunc: CTCPRequestFunc, 59 | CTCPErrorFunc: CTCPErrorFunc, 60 | CUDPRequestFunc: CUDPRequestFunc, 61 | CUDPErrorFunc: CUDPErrorFunc, 62 | udpSessionMap: make(map[uint32]transport.PUDPConn), 63 | } 64 | if UpCounterVec != nil && DownCounterVec != nil && ConnGaugeVec != nil { 65 | authB64 := base64.StdEncoding.EncodeToString(auth) 66 | sc.UpCounter = UpCounterVec.WithLabelValues(authB64) 67 | sc.DownCounter = DownCounterVec.WithLabelValues(authB64) 68 | sc.ConnGauge = ConnGaugeVec.WithLabelValues(authB64) 69 | } 70 | return sc 71 | } 72 | 73 | func (c *serverClient) Run() error { 74 | if !c.DisableUDP { 75 | go func() { 76 | for { 77 | msg, err := c.CS.ReceiveMessage() 78 | if err != nil { 79 | break 80 | } 81 | c.handleMessage(msg) 82 | } 83 | }() 84 | } 85 | for { 86 | stream, err := c.CS.AcceptStream(context.Background()) 87 | if err != nil { 88 | return err 89 | } 90 | if c.ConnGauge != nil { 91 | c.ConnGauge.Inc() 92 | } 93 | go func() { 94 | stream := &wrappedQUICStream{stream} 95 | c.handleStream(stream) 96 | _ = stream.Close() 97 | if c.ConnGauge != nil { 98 | c.ConnGauge.Dec() 99 | } 100 | }() 101 | } 102 | } 103 | 104 | func (c *serverClient) handleStream(stream quic.Stream) { 105 | // Read request 106 | var req clientRequest 107 | err := struc.Unpack(stream, &req) 108 | if err != nil { 109 | return 110 | } 111 | if !req.UDP { 112 | // TCP connection 113 | c.handleTCP(stream, req.Host, req.Port) 114 | } else if !c.DisableUDP { 115 | // UDP connection 116 | c.handleUDP(stream) 117 | } else { 118 | // UDP disabled 119 | _ = struc.Pack(stream, &serverResponse{ 120 | OK: false, 121 | Message: "UDP disabled", 122 | }) 123 | } 124 | } 125 | 126 | func (c *serverClient) handleMessage(msg []byte) { 127 | var udpMsg udpMessage 128 | if c.V2 { 129 | var udpMsgV2 udpMessageV2 130 | err := struc.Unpack(bytes.NewBuffer(msg), &udpMsgV2) 131 | if err != nil { 132 | return 133 | } 134 | udpMsg = udpMessage{ 135 | SessionID: udpMsgV2.SessionID, 136 | HostLen: udpMsgV2.HostLen, 137 | Host: udpMsgV2.Host, 138 | Port: udpMsgV2.Port, 139 | FragCount: 1, 140 | DataLen: udpMsgV2.DataLen, 141 | Data: udpMsgV2.Data, 142 | } 143 | } else { 144 | err := struc.Unpack(bytes.NewBuffer(msg), &udpMsg) 145 | if err != nil { 146 | return 147 | } 148 | } 149 | dfMsg := c.udpDefragger.Feed(udpMsg) 150 | if dfMsg == nil { 151 | return 152 | } 153 | c.udpSessionMutex.RLock() 154 | conn, ok := c.udpSessionMap[dfMsg.SessionID] 155 | c.udpSessionMutex.RUnlock() 156 | if ok { 157 | // Session found, send the message 158 | action, arg := acl.ActionDirect, "" 159 | var isDomain bool 160 | var ipAddr *net.IPAddr 161 | var err error 162 | if c.ACLEngine != nil { 163 | action, arg, isDomain, ipAddr, err = c.ACLEngine.ResolveAndMatch(dfMsg.Host, dfMsg.Port, true) 164 | } else { 165 | ipAddr, isDomain, err = c.Transport.ResolveIPAddr(dfMsg.Host) 166 | } 167 | if err != nil && !(isDomain && c.Transport.SOCKS5Enabled()) { // Special case for domain requests + SOCKS5 outbound 168 | return 169 | } 170 | switch action { 171 | case acl.ActionDirect, acl.ActionProxy: // Treat proxy as direct on server side 172 | addrEx := &transport.AddrEx{ 173 | IPAddr: ipAddr, 174 | Port: int(dfMsg.Port), 175 | } 176 | if isDomain { 177 | addrEx.Domain = dfMsg.Host 178 | } 179 | _, _ = conn.WriteToUDP(dfMsg.Data, addrEx) 180 | if c.UpCounter != nil { 181 | c.UpCounter.Add(float64(len(dfMsg.Data))) 182 | } 183 | case acl.ActionBlock: 184 | // Do nothing 185 | case acl.ActionHijack: 186 | hijackIPAddr, isDomain, err := c.Transport.ResolveIPAddr(arg) 187 | if err == nil || (isDomain && c.Transport.SOCKS5Enabled()) { // Special case for domain requests + SOCKS5 outbound 188 | addrEx := &transport.AddrEx{ 189 | IPAddr: hijackIPAddr, 190 | Port: int(dfMsg.Port), 191 | } 192 | if isDomain { 193 | addrEx.Domain = arg 194 | } 195 | _, _ = conn.WriteToUDP(dfMsg.Data, addrEx) 196 | if c.UpCounter != nil { 197 | c.UpCounter.Add(float64(len(dfMsg.Data))) 198 | } 199 | } 200 | default: 201 | // Do nothing 202 | } 203 | } 204 | } 205 | 206 | func (c *serverClient) handleTCP(stream quic.Stream, host string, port uint16) { 207 | addrStr := net.JoinHostPort(host, strconv.Itoa(int(port))) 208 | action, arg := acl.ActionDirect, "" 209 | var isDomain bool 210 | var ipAddr *net.IPAddr 211 | var err error 212 | if c.ACLEngine != nil { 213 | action, arg, isDomain, ipAddr, err = c.ACLEngine.ResolveAndMatch(host, port, false) 214 | } else { 215 | ipAddr, isDomain, err = c.Transport.ResolveIPAddr(host) 216 | } 217 | if err != nil && !(isDomain && c.Transport.SOCKS5Enabled()) { // Special case for domain requests + SOCKS5 outbound 218 | _ = struc.Pack(stream, &serverResponse{ 219 | OK: false, 220 | Message: "host resolution failure", 221 | }) 222 | c.CTCPErrorFunc(c.ClientAddr, c.Auth, addrStr, err) 223 | return 224 | } 225 | c.CTCPRequestFunc(c.ClientAddr, c.Auth, addrStr, action, arg) 226 | 227 | var conn net.Conn // Connection to be piped 228 | switch action { 229 | case acl.ActionDirect, acl.ActionProxy: // Treat proxy as direct on server side 230 | addrEx := &transport.AddrEx{ 231 | IPAddr: ipAddr, 232 | Port: int(port), 233 | } 234 | if isDomain { 235 | addrEx.Domain = host 236 | } 237 | conn, err = c.Transport.DialTCP(addrEx) 238 | if err != nil { 239 | _ = struc.Pack(stream, &serverResponse{ 240 | OK: false, 241 | Message: err.Error(), 242 | }) 243 | c.CTCPErrorFunc(c.ClientAddr, c.Auth, addrStr, err) 244 | return 245 | } 246 | case acl.ActionBlock: 247 | _ = struc.Pack(stream, &serverResponse{ 248 | OK: false, 249 | Message: "blocked by ACL", 250 | }) 251 | return 252 | case acl.ActionHijack: 253 | hijackIPAddr, isDomain, err := c.Transport.ResolveIPAddr(arg) 254 | if err != nil && !(isDomain && c.Transport.SOCKS5Enabled()) { // Special case for domain requests + SOCKS5 outbound 255 | _ = struc.Pack(stream, &serverResponse{ 256 | OK: false, 257 | Message: err.Error(), 258 | }) 259 | c.CTCPErrorFunc(c.ClientAddr, c.Auth, addrStr, err) 260 | return 261 | } 262 | addrEx := &transport.AddrEx{ 263 | IPAddr: hijackIPAddr, 264 | Port: int(port), 265 | } 266 | if isDomain { 267 | addrEx.Domain = arg 268 | } 269 | conn, err = c.Transport.DialTCP(addrEx) 270 | if err != nil { 271 | _ = struc.Pack(stream, &serverResponse{ 272 | OK: false, 273 | Message: err.Error(), 274 | }) 275 | c.CTCPErrorFunc(c.ClientAddr, c.Auth, addrStr, err) 276 | return 277 | } 278 | default: 279 | _ = struc.Pack(stream, &serverResponse{ 280 | OK: false, 281 | Message: "ACL error", 282 | }) 283 | return 284 | } 285 | // So far so good if we reach here 286 | defer conn.Close() 287 | err = struc.Pack(stream, &serverResponse{ 288 | OK: true, 289 | }) 290 | if err != nil { 291 | return 292 | } 293 | if c.UpCounter != nil && c.DownCounter != nil { 294 | err = utils.Pipe2Way(stream, conn, func(i int) { 295 | if i > 0 { 296 | c.UpCounter.Add(float64(i)) 297 | } else { 298 | c.DownCounter.Add(float64(-i)) 299 | } 300 | }) 301 | } else { 302 | err = utils.Pipe2Way(stream, conn, nil) 303 | } 304 | c.CTCPErrorFunc(c.ClientAddr, c.Auth, addrStr, err) 305 | } 306 | 307 | func (c *serverClient) handleUDP(stream quic.Stream) { 308 | // Like in SOCKS5, the stream here is only used to maintain the UDP session. No need to read anything from it 309 | conn, err := c.Transport.ListenUDP() 310 | if err != nil { 311 | _ = struc.Pack(stream, &serverResponse{ 312 | OK: false, 313 | Message: "UDP initialization failed", 314 | }) 315 | c.CUDPErrorFunc(c.ClientAddr, c.Auth, 0, err) 316 | return 317 | } 318 | defer conn.Close() 319 | 320 | var id uint32 321 | c.udpSessionMutex.Lock() 322 | id = c.nextUDPSessionID 323 | c.udpSessionMap[id] = conn 324 | c.nextUDPSessionID += 1 325 | c.udpSessionMutex.Unlock() 326 | 327 | err = struc.Pack(stream, &serverResponse{ 328 | OK: true, 329 | UDPSessionID: id, 330 | }) 331 | if err != nil { 332 | return 333 | } 334 | c.CUDPRequestFunc(c.ClientAddr, c.Auth, id) 335 | 336 | // Receive UDP packets, send them to the client 337 | go func() { 338 | buf := make([]byte, udpBufferSize) 339 | for { 340 | n, rAddr, err := conn.ReadFromUDP(buf) 341 | if n > 0 { 342 | var msgBuf bytes.Buffer 343 | if c.V2 { 344 | msg := udpMessageV2{ 345 | SessionID: id, 346 | Host: rAddr.IP.String(), 347 | Port: uint16(rAddr.Port), 348 | Data: buf[:n], 349 | } 350 | _ = struc.Pack(&msgBuf, &msg) 351 | _ = c.CS.SendMessage(msgBuf.Bytes()) 352 | } else { 353 | msg := udpMessage{ 354 | SessionID: id, 355 | Host: rAddr.IP.String(), 356 | Port: uint16(rAddr.Port), 357 | FragCount: 1, 358 | Data: buf[:n], 359 | } 360 | // try no frag first 361 | _ = struc.Pack(&msgBuf, &msg) 362 | sendErr := c.CS.SendMessage(msgBuf.Bytes()) 363 | if sendErr != nil { 364 | if errSize, ok := sendErr.(quic.ErrMessageToLarge); ok { 365 | // need to frag 366 | msg.MsgID = uint16(rand.Intn(0xFFFF)) + 1 // msgID must be > 0 when fragCount > 1 367 | fragMsgs := fragUDPMessage(msg, int(errSize)) 368 | for _, fragMsg := range fragMsgs { 369 | msgBuf.Reset() 370 | _ = struc.Pack(&msgBuf, &fragMsg) 371 | _ = c.CS.SendMessage(msgBuf.Bytes()) 372 | } 373 | } 374 | } 375 | } 376 | if c.DownCounter != nil { 377 | c.DownCounter.Add(float64(n)) 378 | } 379 | } 380 | if err != nil { 381 | break 382 | } 383 | } 384 | _ = stream.Close() 385 | }() 386 | 387 | // Hold the stream until it's closed by the client 388 | buf := make([]byte, 1024) 389 | for { 390 | _, err = stream.Read(buf) 391 | if err != nil { 392 | break 393 | } 394 | } 395 | c.CUDPErrorFunc(c.ClientAddr, c.Auth, id, err) 396 | 397 | // Remove the session 398 | c.udpSessionMutex.Lock() 399 | delete(c.udpSessionMap, id) 400 | c.udpSessionMutex.Unlock() 401 | } 402 | -------------------------------------------------------------------------------- /cmd/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "errors" 6 | "io" 7 | "net" 8 | "net/http" 9 | "time" 10 | 11 | "github.com/HyNetwork/hysteria/cmd/auth" 12 | "github.com/HyNetwork/hysteria/pkg/acl" 13 | hyCongestion "github.com/HyNetwork/hysteria/pkg/congestion" 14 | "github.com/HyNetwork/hysteria/pkg/core" 15 | "github.com/HyNetwork/hysteria/pkg/obfs" 16 | "github.com/HyNetwork/hysteria/pkg/pmtud_fix" 17 | "github.com/HyNetwork/hysteria/pkg/sockopt" 18 | "github.com/HyNetwork/hysteria/pkg/transport" 19 | "github.com/lucas-clemente/quic-go" 20 | "github.com/lucas-clemente/quic-go/congestion" 21 | "github.com/oschwald/geoip2-golang" 22 | "github.com/prometheus/client_golang/prometheus" 23 | "github.com/prometheus/client_golang/prometheus/promhttp" 24 | "github.com/sirupsen/logrus" 25 | "github.com/yosuke-furukawa/json5/encoding/json5" 26 | ) 27 | 28 | func server(config *serverConfig) { 29 | logrus.WithField("config", config.String()).Info("Server configuration loaded") 30 | // Resolver 31 | if len(config.Resolver) > 0 { 32 | err := setResolver(config.Resolver) 33 | if err != nil { 34 | logrus.WithFields(logrus.Fields{ 35 | "error": err, 36 | }).Fatal("Failed to set resolver") 37 | } 38 | } 39 | // Load TLS config 40 | var tlsConfig *tls.Config 41 | if len(config.ACME.Domains) > 0 { 42 | // ACME mode 43 | tc, err := acmeTLSConfig(config.ACME.Domains, config.ACME.Email, 44 | config.ACME.DisableHTTPChallenge, config.ACME.DisableTLSALPNChallenge, 45 | config.ACME.AltHTTPPort, config.ACME.AltTLSALPNPort) 46 | if err != nil { 47 | logrus.WithFields(logrus.Fields{ 48 | "error": err, 49 | }).Fatal("Failed to get a certificate with ACME") 50 | } 51 | tc.MinVersion = tls.VersionTLS13 52 | tlsConfig = tc 53 | } else { 54 | // Local cert mode 55 | kpl, err := newKeypairLoader(config.CertFile, config.KeyFile) 56 | if err != nil { 57 | logrus.WithFields(logrus.Fields{ 58 | "error": err, 59 | "cert": config.CertFile, 60 | "key": config.KeyFile, 61 | }).Fatal("Failed to load the certificate") 62 | } 63 | tlsConfig = &tls.Config{ 64 | GetCertificate: kpl.GetCertificateFunc(), 65 | MinVersion: tls.VersionTLS13, 66 | } 67 | } 68 | if config.ALPN != "" { 69 | tlsConfig.NextProtos = []string{config.ALPN} 70 | } else { 71 | tlsConfig.NextProtos = []string{DefaultALPN} 72 | } 73 | // QUIC config 74 | quicConfig := &quic.Config{ 75 | InitialStreamReceiveWindow: config.ReceiveWindowConn, 76 | MaxStreamReceiveWindow: config.ReceiveWindowConn, 77 | InitialConnectionReceiveWindow: config.ReceiveWindowClient, 78 | MaxConnectionReceiveWindow: config.ReceiveWindowClient, 79 | MaxIncomingStreams: int64(config.MaxConnClient), 80 | KeepAlivePeriod: KeepAlivePeriod, 81 | DisablePathMTUDiscovery: config.DisableMTUDiscovery, 82 | EnableDatagrams: true, 83 | } 84 | if config.ReceiveWindowConn == 0 { 85 | quicConfig.InitialStreamReceiveWindow = DefaultStreamReceiveWindow 86 | quicConfig.MaxStreamReceiveWindow = DefaultStreamReceiveWindow 87 | } 88 | if config.ReceiveWindowClient == 0 { 89 | quicConfig.InitialConnectionReceiveWindow = DefaultConnectionReceiveWindow 90 | quicConfig.MaxConnectionReceiveWindow = DefaultConnectionReceiveWindow 91 | } 92 | if quicConfig.MaxIncomingStreams == 0 { 93 | quicConfig.MaxIncomingStreams = DefaultMaxIncomingStreams 94 | } 95 | if !quicConfig.DisablePathMTUDiscovery && pmtud_fix.DisablePathMTUDiscovery { 96 | logrus.Info("Path MTU Discovery is not yet supported on this platform") 97 | } 98 | // Auth 99 | var authFunc core.ConnectFunc 100 | var err error 101 | switch authMode := config.Auth.Mode; authMode { 102 | case "", "none": 103 | if len(config.Obfs) == 0 { 104 | logrus.Warn("No authentication or obfuscation enabled. " + 105 | "Your server could be accessed by anyone! Are you sure this is what you intended?") 106 | } 107 | authFunc = func(addr net.Addr, auth []byte, sSend uint64, sRecv uint64) (bool, string) { 108 | return true, "Welcome" 109 | } 110 | case "password", "passwords": 111 | authFunc, err = passwordAuthFunc(config.Auth.Config) 112 | if err != nil { 113 | logrus.WithFields(logrus.Fields{ 114 | "error": err, 115 | }).Fatal("Failed to enable password authentication") 116 | } else { 117 | logrus.Info("Password authentication enabled") 118 | } 119 | case "external": 120 | authFunc, err = externalAuthFunc(config.Auth.Config) 121 | if err != nil { 122 | logrus.WithFields(logrus.Fields{ 123 | "error": err, 124 | }).Fatal("Failed to enable external authentication") 125 | } else { 126 | logrus.Info("External authentication enabled") 127 | } 128 | default: 129 | logrus.WithField("mode", config.Auth.Mode).Fatal("Unsupported authentication mode") 130 | } 131 | connectFunc := func(addr net.Addr, auth []byte, sSend uint64, sRecv uint64) (bool, string) { 132 | ok, msg := authFunc(addr, auth, sSend, sRecv) 133 | if !ok { 134 | logrus.WithFields(logrus.Fields{ 135 | "src": defaultIPMasker.Mask(addr.String()), 136 | "msg": msg, 137 | }).Info("Authentication failed, client rejected") 138 | } else { 139 | logrus.WithFields(logrus.Fields{ 140 | "src": defaultIPMasker.Mask(addr.String()), 141 | }).Info("Client connected") 142 | } 143 | return ok, msg 144 | } 145 | // Obfuscator 146 | var obfuscator obfs.Obfuscator 147 | if len(config.Obfs) > 0 { 148 | obfuscator = obfs.NewXPlusObfuscator([]byte(config.Obfs)) 149 | } 150 | // Resolve preference 151 | if len(config.ResolvePreference) > 0 { 152 | pref, err := transport.ResolvePreferenceFromString(config.ResolvePreference) 153 | if err != nil { 154 | logrus.WithFields(logrus.Fields{ 155 | "error": err, 156 | }).Fatal("Failed to parse the resolve preference") 157 | } 158 | transport.DefaultServerTransport.ResolvePreference = pref 159 | } 160 | // SOCKS5 outbound 161 | if config.SOCKS5Outbound.Server != "" { 162 | ob, err := transport.NewSOCKS5Client(config.SOCKS5Outbound.Server, 163 | config.SOCKS5Outbound.User, config.SOCKS5Outbound.Password, 10*time.Second) 164 | if err != nil { 165 | logrus.WithFields(logrus.Fields{ 166 | "error": err, 167 | }).Fatal("Failed to initialize SOCKS5 outbound") 168 | } 169 | transport.DefaultServerTransport.SOCKS5Client = ob 170 | } 171 | // Bind outbound 172 | if config.BindOutbound.Device != "" { 173 | iface, err := net.InterfaceByName(config.BindOutbound.Device) 174 | if err != nil { 175 | logrus.WithFields(logrus.Fields{ 176 | "error": err, 177 | }).Fatal("Failed to find the interface") 178 | } 179 | transport.DefaultServerTransport.LocalUDPIntf = iface 180 | sockopt.BindDialer(transport.DefaultServerTransport.Dialer, iface) 181 | } 182 | if config.BindOutbound.Address != "" { 183 | ip := net.ParseIP(config.BindOutbound.Address) 184 | if ip == nil { 185 | logrus.WithFields(logrus.Fields{ 186 | "error": err, 187 | }).Fatal("Failed to parse the address") 188 | } 189 | transport.DefaultServerTransport.Dialer.LocalAddr = &net.TCPAddr{IP: ip} 190 | transport.DefaultServerTransport.LocalUDPAddr = &net.UDPAddr{IP: ip} 191 | } 192 | // ACL 193 | var aclEngine *acl.Engine 194 | if len(config.ACL) > 0 { 195 | aclEngine, err = acl.LoadFromFile(config.ACL, func(addr string) (*net.IPAddr, error) { 196 | ipAddr, _, err := transport.DefaultServerTransport.ResolveIPAddr(addr) 197 | return ipAddr, err 198 | }, 199 | func() (*geoip2.Reader, error) { 200 | if len(config.MMDB) > 0 { 201 | return loadMMDBReader(config.MMDB) 202 | } else { 203 | return loadMMDBReader(DefaultMMDBFilename) 204 | } 205 | }) 206 | if err != nil { 207 | logrus.WithFields(logrus.Fields{ 208 | "error": err, 209 | "file": config.ACL, 210 | }).Fatal("Failed to parse ACL") 211 | } 212 | aclEngine.DefaultAction = acl.ActionDirect 213 | } 214 | // Server 215 | var promReg *prometheus.Registry 216 | if len(config.PrometheusListen) > 0 { 217 | promReg = prometheus.NewRegistry() 218 | go func() { 219 | http.Handle("/metrics", promhttp.HandlerFor(promReg, promhttp.HandlerOpts{})) 220 | err := http.ListenAndServe(config.PrometheusListen, nil) 221 | logrus.WithField("error", err).Fatal("Prometheus HTTP server error") 222 | }() 223 | } 224 | up, down, _ := config.Speed() 225 | server, err := core.NewServer(config.Listen, config.Protocol, tlsConfig, quicConfig, transport.DefaultServerTransport, 226 | up, down, 227 | func(refBPS uint64) congestion.CongestionControl { 228 | return hyCongestion.NewBrutalSender(congestion.ByteCount(refBPS)) 229 | }, config.DisableUDP, aclEngine, obfuscator, connectFunc, disconnectFunc, 230 | tcpRequestFunc, tcpErrorFunc, udpRequestFunc, udpErrorFunc, promReg) 231 | if err != nil { 232 | logrus.WithField("error", err).Fatal("Failed to initialize server") 233 | } 234 | defer server.Close() 235 | logrus.WithField("addr", config.Listen).Info("Server up and running") 236 | 237 | err = server.Serve() 238 | logrus.WithField("error", err).Fatal("Server shutdown") 239 | } 240 | 241 | func passwordAuthFunc(rawMsg json5.RawMessage) (core.ConnectFunc, error) { 242 | var pwds []string 243 | err := json5.Unmarshal(rawMsg, &pwds) 244 | if err != nil { 245 | // not a string list, legacy format? 246 | var pwdConfig map[string]string 247 | err = json5.Unmarshal(rawMsg, &pwdConfig) 248 | if err != nil || len(pwdConfig["password"]) == 0 { 249 | // still no, invalid config 250 | return nil, errors.New("invalid config") 251 | } 252 | // yes it is 253 | pwds = []string{pwdConfig["password"]} 254 | } 255 | return func(addr net.Addr, auth []byte, sSend uint64, sRecv uint64) (bool, string) { 256 | for _, pwd := range pwds { 257 | if string(auth) == pwd { 258 | return true, "Welcome" 259 | } 260 | } 261 | return false, "Wrong password" 262 | }, nil 263 | } 264 | 265 | func externalAuthFunc(rawMsg json5.RawMessage) (core.ConnectFunc, error) { 266 | var extConfig map[string]string 267 | err := json5.Unmarshal(rawMsg, &extConfig) 268 | if err != nil { 269 | return nil, errors.New("invalid config") 270 | } 271 | if len(extConfig["http"]) != 0 { 272 | hp := &auth.HTTPAuthProvider{ 273 | Client: &http.Client{ 274 | Timeout: 10 * time.Second, 275 | }, 276 | URL: extConfig["http"], 277 | } 278 | return hp.Auth, nil 279 | } else if len(extConfig["cmd"]) != 0 { 280 | cp := &auth.CmdAuthProvider{ 281 | Cmd: extConfig["cmd"], 282 | } 283 | return cp.Auth, nil 284 | } else { 285 | return nil, errors.New("invalid config") 286 | } 287 | } 288 | 289 | func disconnectFunc(addr net.Addr, auth []byte, err error) { 290 | logrus.WithFields(logrus.Fields{ 291 | "src": defaultIPMasker.Mask(addr.String()), 292 | "error": err, 293 | }).Info("Client disconnected") 294 | } 295 | 296 | func tcpRequestFunc(addr net.Addr, auth []byte, reqAddr string, action acl.Action, arg string) { 297 | logrus.WithFields(logrus.Fields{ 298 | "src": defaultIPMasker.Mask(addr.String()), 299 | "dst": defaultIPMasker.Mask(reqAddr), 300 | "action": actionToString(action, arg), 301 | }).Debug("TCP request") 302 | } 303 | 304 | func tcpErrorFunc(addr net.Addr, auth []byte, reqAddr string, err error) { 305 | if err != io.EOF { 306 | logrus.WithFields(logrus.Fields{ 307 | "src": defaultIPMasker.Mask(addr.String()), 308 | "dst": defaultIPMasker.Mask(reqAddr), 309 | "error": err, 310 | }).Info("TCP error") 311 | } else { 312 | logrus.WithFields(logrus.Fields{ 313 | "src": defaultIPMasker.Mask(addr.String()), 314 | "dst": defaultIPMasker.Mask(reqAddr), 315 | }).Debug("TCP EOF") 316 | } 317 | } 318 | 319 | func udpRequestFunc(addr net.Addr, auth []byte, sessionID uint32) { 320 | logrus.WithFields(logrus.Fields{ 321 | "src": defaultIPMasker.Mask(addr.String()), 322 | "session": sessionID, 323 | }).Debug("UDP request") 324 | } 325 | 326 | func udpErrorFunc(addr net.Addr, auth []byte, sessionID uint32, err error) { 327 | if err != io.EOF { 328 | logrus.WithFields(logrus.Fields{ 329 | "src": defaultIPMasker.Mask(addr.String()), 330 | "session": sessionID, 331 | "error": err, 332 | }).Info("UDP error") 333 | } else { 334 | logrus.WithFields(logrus.Fields{ 335 | "src": defaultIPMasker.Mask(addr.String()), 336 | "session": sessionID, 337 | }).Debug("UDP EOF") 338 | } 339 | } 340 | 341 | func actionToString(action acl.Action, arg string) string { 342 | switch action { 343 | case acl.ActionDirect: 344 | return "Direct" 345 | case acl.ActionProxy: 346 | return "Proxy" 347 | case acl.ActionBlock: 348 | return "Block" 349 | case acl.ActionHijack: 350 | return "Hijack to " + arg 351 | default: 352 | return "Unknown" 353 | } 354 | } 355 | 356 | func parseServerConfig(cb []byte) (*serverConfig, error) { 357 | var c serverConfig 358 | err := json5.Unmarshal(cb, &c) 359 | if err != nil { 360 | return nil, err 361 | } 362 | return &c, c.Check() 363 | } 364 | --------------------------------------------------------------------------------