refactor: re-org packages

This commit is contained in:
Toby 2022-11-24 00:22:44 -08:00
parent e338ed60cb
commit 3184c42956
70 changed files with 110 additions and 107 deletions

97
app/auth/external.go Normal file
View file

@ -0,0 +1,97 @@
package auth
import (
"bytes"
"encoding/json"
"io/ioutil"
"net"
"net/http"
"os/exec"
"strconv"
"strings"
"github.com/sirupsen/logrus"
)
type CmdAuthProvider struct {
Cmd string
}
func (p *CmdAuthProvider) Auth(addr net.Addr, auth []byte, sSend uint64, sRecv uint64) (bool, string) {
cmd := exec.Command(p.Cmd, addr.String(), string(auth), strconv.Itoa(int(sSend)), strconv.Itoa(int(sRecv)))
out, err := cmd.Output()
if err != nil {
if _, ok := err.(*exec.ExitError); ok {
return false, strings.TrimSpace(string(out))
} else {
logrus.WithFields(logrus.Fields{
"error": err,
}).Error("Failed to execute auth command")
return false, "internal error"
}
} else {
return true, strings.TrimSpace(string(out))
}
}
type HTTPAuthProvider struct {
Client *http.Client
URL string
}
type authReq struct {
Addr string `json:"addr"`
Payload []byte `json:"payload"`
Send uint64 `json:"send"`
Recv uint64 `json:"recv"`
}
type authResp struct {
OK bool `json:"ok"`
Msg string `json:"msg"`
}
func (p *HTTPAuthProvider) Auth(addr net.Addr, auth []byte, sSend uint64, sRecv uint64) (bool, string) {
jbs, err := json.Marshal(&authReq{
Addr: addr.String(),
Payload: auth,
Send: sSend,
Recv: sRecv,
})
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
}).Error("Failed to marshal auth request")
return false, "internal error"
}
resp, err := p.Client.Post(p.URL, "application/json", bytes.NewBuffer(jbs))
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
}).Error("Failed to send auth request")
return false, "internal error"
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
logrus.WithFields(logrus.Fields{
"code": resp.StatusCode,
}).Error("Invalid status code from auth server")
return false, "internal error"
}
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
}).Error("Failed to read auth response")
return false, "internal error"
}
var ar authResp
err = json.Unmarshal(data, &ar)
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
}).Error("Failed to unmarshal auth response")
return false, "internal error"
}
return ar.OK, ar.Msg
}

59
app/auth/funcs.go Normal file
View file

@ -0,0 +1,59 @@
package auth
import (
"errors"
"net"
"net/http"
"time"
"github.com/apernet/hysteria/core/cs"
"github.com/yosuke-furukawa/json5/encoding/json5"
)
func PasswordAuthFunc(rawMsg json5.RawMessage) (cs.ConnectFunc, error) {
var pwds []string
err := json5.Unmarshal(rawMsg, &pwds)
if err != nil {
// not a string list, legacy format?
var pwdConfig map[string]string
err = json5.Unmarshal(rawMsg, &pwdConfig)
if err != nil || len(pwdConfig["password"]) == 0 {
// still no, invalid config
return nil, errors.New("invalid config")
}
// yes it is
pwds = []string{pwdConfig["password"]}
}
return func(addr net.Addr, auth []byte, sSend uint64, sRecv uint64) (bool, string) {
for _, pwd := range pwds {
if string(auth) == pwd {
return true, "Welcome"
}
}
return false, "Wrong password"
}, nil
}
func ExternalAuthFunc(rawMsg json5.RawMessage) (cs.ConnectFunc, error) {
var extConfig map[string]string
err := json5.Unmarshal(rawMsg, &extConfig)
if err != nil {
return nil, errors.New("invalid config")
}
if len(extConfig["http"]) != 0 {
hp := &HTTPAuthProvider{
Client: &http.Client{
Timeout: 10 * time.Second,
},
URL: extConfig["http"],
}
return hp.Auth, nil
} else if len(extConfig["cmd"]) != 0 {
cp := &CmdAuthProvider{
Cmd: extConfig["cmd"],
}
return cp.Auth, nil
} else {
return nil, errors.New("invalid config")
}
}

74
app/cmd/acme.go Normal file
View file

@ -0,0 +1,74 @@
package main
import (
"context"
"crypto/tls"
"os"
"path/filepath"
"runtime"
"go.uber.org/zap"
"github.com/caddyserver/certmagic"
)
func acmeTLSConfig(domains []string, email string, disableHTTP bool, disableTLSALPN bool,
altHTTPPort int, altTLSALPNPort int,
) (*tls.Config, error) {
cfg := &certmagic.Config{
RenewalWindowRatio: certmagic.DefaultRenewalWindowRatio,
KeySource: certmagic.DefaultKeyGenerator,
Storage: &certmagic.FileStorage{Path: dataDir()},
Logger: zap.NewNop(),
}
issuer := certmagic.NewACMEIssuer(cfg, certmagic.ACMEIssuer{
CA: certmagic.LetsEncryptProductionCA,
TestCA: certmagic.LetsEncryptStagingCA,
Email: email,
Agreed: true,
DisableHTTPChallenge: disableHTTP,
DisableTLSALPNChallenge: disableTLSALPN,
AltHTTPPort: altHTTPPort,
AltTLSALPNPort: altTLSALPNPort,
Logger: zap.NewNop(),
})
cfg.Issuers = []certmagic.Issuer{issuer}
cache := certmagic.NewCache(certmagic.CacheOptions{
GetConfigForCert: func(cert certmagic.Certificate) (*certmagic.Config, error) {
return cfg, nil
},
Logger: zap.NewNop(),
})
cfg = certmagic.New(cache, *cfg)
err := cfg.ManageSync(context.Background(), domains)
if err != nil {
return nil, err
}
return cfg.TLSConfig(), nil
}
func homeDir() string {
home := os.Getenv("HOME")
if home == "" && runtime.GOOS == "windows" {
drive := os.Getenv("HOMEDRIVE")
path := os.Getenv("HOMEPATH")
home = drive + path
if drive == "" || path == "" {
home = os.Getenv("USERPROFILE")
}
}
if home == "" {
home = "."
}
return home
}
func dataDir() string {
baseDir := filepath.Join(homeDir(), ".local", "share")
if xdgData := os.Getenv("XDG_DATA_HOME"); xdgData != "" {
baseDir = xdgData
}
return filepath.Join(baseDir, "certmagic")
}

453
app/cmd/client.go Normal file
View file

@ -0,0 +1,453 @@
package main
import (
"crypto/tls"
"crypto/x509"
"errors"
"io"
"io/ioutil"
"net"
"net/http"
"os"
"time"
hyHTTP "github.com/apernet/hysteria/app/http"
"github.com/apernet/hysteria/app/redirect"
"github.com/apernet/hysteria/app/relay"
"github.com/apernet/hysteria/app/socks5"
"github.com/apernet/hysteria/app/tproxy"
"github.com/apernet/hysteria/core/pktconns"
"github.com/apernet/hysteria/core/pmtud"
"github.com/oschwald/geoip2-golang"
"github.com/yosuke-furukawa/json5/encoding/json5"
"github.com/apernet/hysteria/core/acl"
"github.com/apernet/hysteria/core/cs"
"github.com/apernet/hysteria/core/transport"
"github.com/lucas-clemente/quic-go"
"github.com/sirupsen/logrus"
)
var clientPacketConnFuncFactoryMap = map[string]pktconns.ClientPacketConnFuncFactory{
"": pktconns.NewClientUDPConnFunc,
"udp": pktconns.NewClientUDPConnFunc,
"wechat": pktconns.NewClientWeChatConnFunc,
"wechat-video": pktconns.NewClientWeChatConnFunc,
"faketcp": pktconns.NewClientFakeTCPConnFunc,
}
func client(config *clientConfig) {
logrus.WithField("config", config.String()).Info("Client configuration loaded")
config.Fill() // Fill default values
// Resolver
if len(config.Resolver) > 0 {
err := setResolver(config.Resolver)
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
}).Fatal("Failed to set resolver")
}
}
// TLS
tlsConfig := &tls.Config{
NextProtos: []string{config.ALPN},
ServerName: config.ServerName,
InsecureSkipVerify: config.Insecure,
MinVersion: tls.VersionTLS13,
}
// Load CA
if len(config.CustomCA) > 0 {
bs, err := ioutil.ReadFile(config.CustomCA)
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
"file": config.CustomCA,
}).Fatal("Failed to load CA")
}
cp := x509.NewCertPool()
if !cp.AppendCertsFromPEM(bs) {
logrus.WithFields(logrus.Fields{
"file": config.CustomCA,
}).Fatal("Failed to parse CA")
}
tlsConfig.RootCAs = cp
}
// QUIC config
quicConfig := &quic.Config{
InitialStreamReceiveWindow: config.ReceiveWindowConn,
MaxStreamReceiveWindow: config.ReceiveWindowConn,
InitialConnectionReceiveWindow: config.ReceiveWindow,
MaxConnectionReceiveWindow: config.ReceiveWindow,
HandshakeIdleTimeout: time.Duration(config.HandshakeTimeout) * time.Second,
MaxIdleTimeout: time.Duration(config.IdleTimeout) * time.Second,
KeepAlivePeriod: time.Duration(config.IdleTimeout) * time.Second * 2 / 5,
DisablePathMTUDiscovery: config.DisableMTUDiscovery,
EnableDatagrams: true,
}
if !quicConfig.DisablePathMTUDiscovery && pmtud.DisablePathMTUDiscovery {
logrus.Info("Path MTU Discovery is not yet supported on this platform")
}
// Auth
var auth []byte
if len(config.Auth) > 0 {
auth = config.Auth
} else {
auth = []byte(config.AuthString)
}
// Packet conn
pktConnFuncFactory := clientPacketConnFuncFactoryMap[config.Protocol]
if pktConnFuncFactory == nil {
logrus.WithFields(logrus.Fields{
"protocol": config.Protocol,
}).Fatal("Unsupported protocol")
}
pktConnFunc := pktConnFuncFactory(config.Obfs, time.Duration(config.HopInterval)*time.Second)
// Resolve preference
if len(config.ResolvePreference) > 0 {
pref, err := transport.ResolvePreferenceFromString(config.ResolvePreference)
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
}).Fatal("Failed to parse the resolve preference")
}
transport.DefaultClientTransport.ResolvePreference = pref
}
// ACL
var aclEngine *acl.Engine
if len(config.ACL) > 0 {
var err error
aclEngine, err = acl.LoadFromFile(config.ACL, transport.DefaultClientTransport.ResolveIPAddr,
func() (*geoip2.Reader, error) {
return loadMMDBReader(config.MMDB)
})
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
"file": config.ACL,
}).Fatal("Failed to parse ACL")
}
}
// Client
var client *cs.Client
try := 0
up, down, _ := config.Speed()
for {
try += 1
c, err := cs.NewClient(config.Server, auth, tlsConfig, quicConfig, pktConnFunc, up, down, config.FastOpen,
func(err error) {
if config.QuitOnDisconnect {
logrus.WithFields(logrus.Fields{
"addr": config.Server,
"error": err,
}).Fatal("Connection to server lost, exiting...")
} else {
logrus.WithFields(logrus.Fields{
"addr": config.Server,
"error": err,
}).Error("Connection to server lost, reconnecting...")
}
})
if err != nil {
logrus.WithField("error", err).Error("Failed to initialize client")
if try <= config.Retry || config.Retry < 0 {
logrus.WithFields(logrus.Fields{
"retry": try,
"interval": config.RetryInterval,
}).Info("Retrying...")
time.Sleep(time.Duration(config.RetryInterval) * time.Second)
} else {
logrus.Fatal("Out of retries, exiting...")
}
} else {
client = c
break
}
}
defer client.Close()
logrus.WithField("addr", config.Server).Info("Connected")
// Local
errChan := make(chan error)
if len(config.SOCKS5.Listen) > 0 {
go func() {
var authFunc func(user, password string) bool
if config.SOCKS5.User != "" && config.SOCKS5.Password != "" {
authFunc = func(user, password string) bool {
return config.SOCKS5.User == user && config.SOCKS5.Password == password
}
}
socks5server, err := socks5.NewServer(client, transport.DefaultClientTransport, config.SOCKS5.Listen,
authFunc, time.Duration(config.SOCKS5.Timeout)*time.Second, aclEngine, config.SOCKS5.DisableUDP,
func(addr net.Addr, reqAddr string, action acl.Action, arg string) {
logrus.WithFields(logrus.Fields{
"action": actionToString(action, arg),
"src": defaultIPMasker.Mask(addr.String()),
"dst": defaultIPMasker.Mask(reqAddr),
}).Debug("SOCKS5 TCP request")
},
func(addr net.Addr, reqAddr string, err error) {
if err != io.EOF {
logrus.WithFields(logrus.Fields{
"error": err,
"src": defaultIPMasker.Mask(addr.String()),
"dst": defaultIPMasker.Mask(reqAddr),
}).Info("SOCKS5 TCP error")
} else {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
"dst": defaultIPMasker.Mask(reqAddr),
}).Debug("SOCKS5 TCP EOF")
}
},
func(addr net.Addr) {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
}).Debug("SOCKS5 UDP associate")
},
func(addr net.Addr, err error) {
if err != io.EOF {
logrus.WithFields(logrus.Fields{
"error": err,
"src": defaultIPMasker.Mask(addr.String()),
}).Info("SOCKS5 UDP error")
} else {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
}).Debug("SOCKS5 UDP EOF")
}
})
if err != nil {
logrus.WithField("error", err).Fatal("Failed to initialize SOCKS5 server")
}
logrus.WithField("addr", config.SOCKS5.Listen).Info("SOCKS5 server up and running")
errChan <- socks5server.ListenAndServe()
}()
}
if len(config.HTTP.Listen) > 0 {
go func() {
var authFunc func(user, password string) bool
if config.HTTP.User != "" && config.HTTP.Password != "" {
authFunc = func(user, password string) bool {
return config.HTTP.User == user && config.HTTP.Password == password
}
}
proxy, err := hyHTTP.NewProxyHTTPServer(client, transport.DefaultClientTransport,
time.Duration(config.HTTP.Timeout)*time.Second, aclEngine, authFunc,
func(reqAddr string, action acl.Action, arg string) {
logrus.WithFields(logrus.Fields{
"action": actionToString(action, arg),
"dst": defaultIPMasker.Mask(reqAddr),
}).Debug("HTTP request")
},
func(reqAddr string, err error) {
logrus.WithFields(logrus.Fields{
"error": err,
"dst": defaultIPMasker.Mask(reqAddr),
}).Info("HTTP error")
})
if err != nil {
logrus.WithField("error", err).Fatal("Failed to initialize HTTP server")
}
if config.HTTP.Cert != "" && config.HTTP.Key != "" {
logrus.WithField("addr", config.HTTP.Listen).Info("HTTPS server up and running")
errChan <- http.ListenAndServeTLS(config.HTTP.Listen, config.HTTP.Cert, config.HTTP.Key, proxy)
} else {
logrus.WithField("addr", config.HTTP.Listen).Info("HTTP server up and running")
errChan <- http.ListenAndServe(config.HTTP.Listen, proxy)
}
}()
}
if len(config.TUN.Name) != 0 {
go startTUN(config, client, errChan)
}
if len(config.TCPRelay.Listen) > 0 {
config.TCPRelays = append(config.TCPRelays, Relay{
Listen: config.TCPRelay.Listen,
Remote: config.TCPRelay.Remote,
Timeout: config.TCPRelay.Timeout,
})
}
if len(config.TCPRelays) > 0 {
for _, tcpr := range config.TCPRelays {
go func(tcpr Relay) {
rl, err := relay.NewTCPRelay(client, tcpr.Listen, tcpr.Remote,
time.Duration(tcpr.Timeout)*time.Second,
func(addr net.Addr) {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
}).Debug("TCP relay request")
},
func(addr net.Addr, err error) {
if err != io.EOF {
logrus.WithFields(logrus.Fields{
"error": err,
"src": defaultIPMasker.Mask(addr.String()),
}).Info("TCP relay error")
} else {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
}).Debug("TCP relay EOF")
}
})
if err != nil {
logrus.WithField("error", err).Fatal("Failed to initialize TCP relay")
}
logrus.WithField("addr", tcpr.Listen).Info("TCP relay up and running")
errChan <- rl.ListenAndServe()
}(tcpr)
}
}
if len(config.UDPRelay.Listen) > 0 {
config.UDPRelays = append(config.UDPRelays, Relay{
Listen: config.UDPRelay.Listen,
Remote: config.UDPRelay.Remote,
Timeout: config.UDPRelay.Timeout,
})
}
if len(config.UDPRelays) > 0 {
for _, udpr := range config.UDPRelays {
go func(udpr Relay) {
rl, err := relay.NewUDPRelay(client, udpr.Listen, udpr.Remote,
time.Duration(udpr.Timeout)*time.Second,
func(addr net.Addr) {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
}).Debug("UDP relay request")
},
func(addr net.Addr, err error) {
if err != relay.ErrTimeout {
logrus.WithFields(logrus.Fields{
"error": err,
"src": defaultIPMasker.Mask(addr.String()),
}).Info("UDP relay error")
} else {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
}).Debug("UDP relay session closed")
}
})
if err != nil {
logrus.WithField("error", err).Fatal("Failed to initialize UDP relay")
}
logrus.WithField("addr", udpr.Listen).Info("UDP relay up and running")
errChan <- rl.ListenAndServe()
}(udpr)
}
}
if len(config.TCPTProxy.Listen) > 0 {
go func() {
rl, err := tproxy.NewTCPTProxy(client, config.TCPTProxy.Listen,
time.Duration(config.TCPTProxy.Timeout)*time.Second,
func(addr, reqAddr net.Addr) {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
"dst": defaultIPMasker.Mask(reqAddr.String()),
}).Debug("TCP TProxy request")
},
func(addr, reqAddr net.Addr, err error) {
if err != io.EOF {
logrus.WithFields(logrus.Fields{
"error": err,
"src": defaultIPMasker.Mask(addr.String()),
"dst": defaultIPMasker.Mask(reqAddr.String()),
}).Info("TCP TProxy error")
} else {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
"dst": defaultIPMasker.Mask(reqAddr.String()),
}).Debug("TCP TProxy EOF")
}
})
if err != nil {
logrus.WithField("error", err).Fatal("Failed to initialize TCP TProxy")
}
logrus.WithField("addr", config.TCPTProxy.Listen).Info("TCP TProxy up and running")
errChan <- rl.ListenAndServe()
}()
}
if len(config.UDPTProxy.Listen) > 0 {
go func() {
rl, err := tproxy.NewUDPTProxy(client, config.UDPTProxy.Listen,
time.Duration(config.UDPTProxy.Timeout)*time.Second,
func(addr, reqAddr net.Addr) {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
"dst": defaultIPMasker.Mask(reqAddr.String()),
}).Debug("UDP TProxy request")
},
func(addr, reqAddr net.Addr, err error) {
if !errors.Is(err, os.ErrDeadlineExceeded) {
logrus.WithFields(logrus.Fields{
"error": err,
"src": defaultIPMasker.Mask(addr.String()),
"dst": defaultIPMasker.Mask(reqAddr.String()),
}).Info("UDP TProxy error")
} else {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
"dst": defaultIPMasker.Mask(reqAddr.String()),
}).Debug("UDP TProxy session closed")
}
})
if err != nil {
logrus.WithField("error", err).Fatal("Failed to initialize UDP TProxy")
}
logrus.WithField("addr", config.UDPTProxy.Listen).Info("UDP TProxy up and running")
errChan <- rl.ListenAndServe()
}()
}
if len(config.TCPRedirect.Listen) > 0 {
go func() {
rl, err := redirect.NewTCPRedirect(client, config.TCPRedirect.Listen,
time.Duration(config.TCPRedirect.Timeout)*time.Second,
func(addr, reqAddr net.Addr) {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
"dst": defaultIPMasker.Mask(reqAddr.String()),
}).Debug("TCP Redirect request")
},
func(addr, reqAddr net.Addr, err error) {
if err != io.EOF {
logrus.WithFields(logrus.Fields{
"error": err,
"src": defaultIPMasker.Mask(addr.String()),
"dst": defaultIPMasker.Mask(reqAddr.String()),
}).Info("TCP Redirect error")
} else {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
"dst": defaultIPMasker.Mask(reqAddr.String()),
}).Debug("TCP Redirect EOF")
}
})
if err != nil {
logrus.WithField("error", err).Fatal("Failed to initialize TCP Redirect")
}
logrus.WithField("addr", config.TCPRedirect.Listen).Info("TCP Redirect up and running")
errChan <- rl.ListenAndServe()
}()
}
err := <-errChan
logrus.WithField("error", err).Fatal("Client shutdown")
}
func parseClientConfig(cb []byte) (*clientConfig, error) {
var c clientConfig
err := json5.Unmarshal(cb, &c)
if err != nil {
return nil, err
}
return &c, c.Check()
}

117
app/cmd/client_gpl.go Normal file
View file

@ -0,0 +1,117 @@
//go:build gpl
// +build gpl
package main
import (
"io"
"net"
"strings"
"time"
"github.com/apernet/hysteria/app/tun"
"github.com/docker/go-units"
"gvisor.dev/gvisor/pkg/tcpip/transport/tcp"
"github.com/apernet/hysteria/core/cs"
"github.com/sirupsen/logrus"
)
const license = `Hysteria is a feature-packed proxy & relay utility optimized for lossy, unstable connections.
Copyright (C) 2022 Toby
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
`
func startTUN(config *clientConfig, client *cs.Client, errChan chan error) {
timeout := time.Duration(config.TUN.Timeout) * time.Second
if timeout == 0 {
timeout = 300 * time.Second
}
var err error
var tcpSendBufferSize, tcpReceiveBufferSize int64
if config.TUN.TCPSendBufferSize != "" {
tcpSendBufferSize, err = units.RAMInBytes(config.TUN.TCPSendBufferSize)
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
"tcp-sndbuf": config.TUN.TCPSendBufferSize,
}).Fatal("Failed to parse tcp-sndbuf in the TUN config")
}
if (tcpSendBufferSize != 0 && tcpSendBufferSize < tcp.MinBufferSize) || tcpSendBufferSize > tcp.MaxBufferSize {
logrus.WithFields(logrus.Fields{
"tcp-sndbuf": config.TUN.TCPSendBufferSize,
}).Fatal("Invalid tcp-sndbuf in the TUN config")
}
}
if config.TUN.TCPReceiveBufferSize != "" {
tcpReceiveBufferSize, err = units.RAMInBytes(config.TUN.TCPReceiveBufferSize)
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
"tcp-rcvbuf": config.TUN.TCPReceiveBufferSize,
}).Fatal("Failed to parse tcp-rcvbuf in the TUN config")
}
if (tcpReceiveBufferSize != 0 && tcpReceiveBufferSize < tcp.MinBufferSize) || tcpReceiveBufferSize > tcp.MaxBufferSize {
logrus.WithFields(logrus.Fields{
"error": err,
"tcp-rcvbuf": config.TUN.TCPReceiveBufferSize,
}).Fatal("Invalid tcp-rcvbuf in the TUN config")
}
}
tunServer, err := tun.NewServer(client, timeout,
config.TUN.Name, config.TUN.MTU,
int(tcpSendBufferSize), int(tcpReceiveBufferSize), config.TUN.TCPModerateReceiveBuffer)
if err != nil {
logrus.WithField("error", err).Fatal("Failed to initialize TUN server")
}
tunServer.RequestFunc = func(addr net.Addr, reqAddr string) {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
"dst": defaultIPMasker.Mask(reqAddr),
}).Debugf("TUN %s request", strings.ToUpper(addr.Network()))
}
tunServer.ErrorFunc = func(addr net.Addr, reqAddr string, err error) {
if err != nil {
if err == io.EOF {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
"dst": defaultIPMasker.Mask(reqAddr),
}).Debugf("TUN %s EOF", strings.ToUpper(addr.Network()))
} else if err == cs.ErrClosed && strings.HasPrefix(addr.Network(), "udp") {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
"dst": defaultIPMasker.Mask(reqAddr),
}).Debugf("TUN %s closed for timeout", strings.ToUpper(addr.Network()))
} else if nErr, ok := err.(net.Error); ok && nErr.Timeout() && strings.HasPrefix(addr.Network(), "tcp") {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
"dst": defaultIPMasker.Mask(reqAddr),
}).Debugf("TUN %s closed for timeout", strings.ToUpper(addr.Network()))
} else {
logrus.WithFields(logrus.Fields{
"error": err,
"src": defaultIPMasker.Mask(addr.String()),
"dst": defaultIPMasker.Mask(reqAddr),
}).Infof("TUN %s error", strings.ToUpper(addr.Network()))
}
}
}
logrus.WithField("interface", config.TUN.Name).Info("TUN up and running")
errChan <- tunServer.ListenAndServe()
}

36
app/cmd/client_nongpl.go Normal file
View file

@ -0,0 +1,36 @@
//go:build !gpl
// +build !gpl
package main
import (
"github.com/apernet/hysteria/core/cs"
"github.com/sirupsen/logrus"
)
const license = `The MIT License (MIT)
Copyright (c) 2021 Toby
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
`
func startTUN(config *clientConfig, client *cs.Client, errChan chan error) {
logrus.Fatalln("TUN mode is only available in GPL builds. Please rebuild hysteria with -tags gpl")
}

67
app/cmd/completion.go Normal file
View file

@ -0,0 +1,67 @@
package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var completionCmd = &cobra.Command{
Use: "completion [bash|zsh|fish|powershell]",
Short: "Generate completion script",
Long: fmt.Sprintf(`To load completions:
Bash:
$ source <(%[1]s completion bash)
# To load completions for each session, execute once:
# Linux:
$ %[1]s completion bash > /etc/bash_completion.d/%[1]s
# macOS:
$ %[1]s completion bash > /usr/local/etc/bash_completion.d/%[1]s
Zsh:
# If shell completion is not already enabled in your environment,
# you will need to enable it. You can execute the following once:
$ echo "autoload -U compinit; compinit" >> ~/.zshrc
# To load completions for each session, execute once:
$ %[1]s completion zsh > "${fpath[1]}/_%[1]s"
# You will need to start a new shell for this setup to take effect.
fish:
$ %[1]s completion fish | source
# To load completions for each session, execute once:
$ %[1]s completion fish > ~/.config/fish/completions/%[1]s.fish
PowerShell:
PS> %[1]s completion powershell | Out-String | Invoke-Expression
# To load completions for every new session, run:
PS> %[1]s completion powershell > %[1]s.ps1
# and source this file from your PowerShell profile.
`, rootCmd.Name()),
DisableFlagsInUseLine: true,
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
Args: cobra.ExactValidArgs(1),
Run: func(cmd *cobra.Command, args []string) {
switch args[0] {
case "bash":
_ = cmd.Root().GenBashCompletion(os.Stdout)
case "zsh":
_ = cmd.Root().GenZshCompletion(os.Stdout)
case "fish":
_ = cmd.Root().GenFishCompletion(os.Stdout, true)
case "powershell":
_ = cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
}
},
}

381
app/cmd/config.go Normal file
View file

@ -0,0 +1,381 @@
package main
import (
"errors"
"fmt"
"regexp"
"strconv"
"github.com/sirupsen/logrus"
"github.com/yosuke-furukawa/json5/encoding/json5"
)
const (
mbpsToBps = 125000
minSpeedBPS = 16384
DefaultALPN = "hysteria"
DefaultStreamReceiveWindow = 16777216 // 16 MB
DefaultConnectionReceiveWindow = DefaultStreamReceiveWindow * 5 / 2 // 40 MB
DefaultMaxIncomingStreams = 1024
DefaultMMDBFilename = "GeoLite2-Country.mmdb"
ServerMaxIdleTimeoutSec = 60
DefaultClientIdleTimeoutSec = 20
DefaultClientHopIntervalSec = 10
)
var rateStringRegexp = regexp.MustCompile(`^(\d+)\s*([KMGT]?)([Bb])ps$`)
type serverConfig struct {
Listen string `json:"listen"`
Protocol string `json:"protocol"`
ACME struct {
Domains []string `json:"domains"`
Email string `json:"email"`
DisableHTTPChallenge bool `json:"disable_http"`
DisableTLSALPNChallenge bool `json:"disable_tlsalpn"`
AltHTTPPort int `json:"alt_http_port"`
AltTLSALPNPort int `json:"alt_tlsalpn_port"`
} `json:"acme"`
CertFile string `json:"cert"`
KeyFile string `json:"key"`
// Optional below
Up string `json:"up"`
UpMbps int `json:"up_mbps"`
Down string `json:"down"`
DownMbps int `json:"down_mbps"`
DisableUDP bool `json:"disable_udp"`
ACL string `json:"acl"`
MMDB string `json:"mmdb"`
Obfs string `json:"obfs"`
Auth struct {
Mode string `json:"mode"`
Config json5.RawMessage `json:"config"`
} `json:"auth"`
ALPN string `json:"alpn"`
PrometheusListen string `json:"prometheus_listen"`
ReceiveWindowConn uint64 `json:"recv_window_conn"`
ReceiveWindowClient uint64 `json:"recv_window_client"`
MaxConnClient int `json:"max_conn_client"`
DisableMTUDiscovery bool `json:"disable_mtu_discovery"`
Resolver string `json:"resolver"`
ResolvePreference string `json:"resolve_preference"`
SOCKS5Outbound struct {
Server string `json:"server"`
User string `json:"user"`
Password string `json:"password"`
} `json:"socks5_outbound"`
BindOutbound struct {
Address string `json:"address"`
Device string `json:"device"`
} `json:"bind_outbound"`
}
func (c *serverConfig) Speed() (uint64, uint64, error) {
var up, down uint64
if len(c.Up) > 0 {
up = stringToBps(c.Up)
if up == 0 {
return 0, 0, errors.New("invalid speed format")
}
} else {
up = uint64(c.UpMbps) * mbpsToBps
}
if len(c.Down) > 0 {
down = stringToBps(c.Down)
if down == 0 {
return 0, 0, errors.New("invalid speed format")
}
} else {
down = uint64(c.DownMbps) * mbpsToBps
}
return up, down, nil
}
func (c *serverConfig) Check() error {
if len(c.Listen) == 0 {
return errors.New("missing listen address")
}
if len(c.ACME.Domains) == 0 && (len(c.CertFile) == 0 || len(c.KeyFile) == 0) {
return errors.New("need either ACME info or cert/key files")
}
if up, down, err := c.Speed(); err != nil || (up != 0 && up < minSpeedBPS) || (down != 0 && down < minSpeedBPS) {
return errors.New("invalid speed")
}
if (c.ReceiveWindowConn != 0 && c.ReceiveWindowConn < 65536) ||
(c.ReceiveWindowClient != 0 && c.ReceiveWindowClient < 65536) {
return errors.New("invalid receive window size")
}
if c.MaxConnClient < 0 {
return errors.New("invalid max connections per client")
}
return nil
}
func (c *serverConfig) Fill() {
if len(c.ALPN) == 0 {
c.ALPN = DefaultALPN
}
if c.ReceiveWindowConn == 0 {
c.ReceiveWindowConn = DefaultStreamReceiveWindow
}
if c.ReceiveWindowClient == 0 {
c.ReceiveWindowClient = DefaultConnectionReceiveWindow
}
if c.MaxConnClient == 0 {
c.MaxConnClient = DefaultMaxIncomingStreams
}
if len(c.MMDB) == 0 {
c.MMDB = DefaultMMDBFilename
}
}
func (c *serverConfig) String() string {
return fmt.Sprintf("%+v", *c)
}
type Relay struct {
Listen string `json:"listen"`
Remote string `json:"remote"`
Timeout int `json:"timeout"`
}
func (r *Relay) Check() error {
if len(r.Listen) == 0 {
return errors.New("missing relay listen address")
}
if len(r.Remote) == 0 {
return errors.New("missing relay remote address")
}
if r.Timeout != 0 && r.Timeout < 4 {
return errors.New("invalid relay timeout")
}
return nil
}
type clientConfig struct {
Server string `json:"server"`
Protocol string `json:"protocol"`
Up string `json:"up"`
UpMbps int `json:"up_mbps"`
Down string `json:"down"`
DownMbps int `json:"down_mbps"`
// Optional below
Retry int `json:"retry"`
RetryInterval int `json:"retry_interval"`
QuitOnDisconnect bool `json:"quit_on_disconnect"`
HandshakeTimeout int `json:"handshake_timeout"`
IdleTimeout int `json:"idle_timeout"`
HopInterval int `json:"hop_interval"`
SOCKS5 struct {
Listen string `json:"listen"`
Timeout int `json:"timeout"`
DisableUDP bool `json:"disable_udp"`
User string `json:"user"`
Password string `json:"password"`
} `json:"socks5"`
HTTP struct {
Listen string `json:"listen"`
Timeout int `json:"timeout"`
User string `json:"user"`
Password string `json:"password"`
Cert string `json:"cert"`
Key string `json:"key"`
} `json:"http"`
TUN struct {
Name string `json:"name"`
Timeout int `json:"timeout"`
MTU uint32 `json:"mtu"`
TCPSendBufferSize string `json:"tcp_sndbuf"`
TCPReceiveBufferSize string `json:"tcp_rcvbuf"`
TCPModerateReceiveBuffer bool `json:"tcp_autotuning"`
} `json:"tun"`
TCPRelays []Relay `json:"relay_tcps"`
TCPRelay Relay `json:"relay_tcp"` // deprecated, but we still support it for backward compatibility
UDPRelays []Relay `json:"relay_udps"`
UDPRelay Relay `json:"relay_udp"` // deprecated, but we still support it for backward compatibility
TCPTProxy struct {
Listen string `json:"listen"`
Timeout int `json:"timeout"`
} `json:"tproxy_tcp"`
UDPTProxy struct {
Listen string `json:"listen"`
Timeout int `json:"timeout"`
} `json:"tproxy_udp"`
TCPRedirect struct {
Listen string `json:"listen"`
Timeout int `json:"timeout"`
} `json:"redirect_tcp"`
ACL string `json:"acl"`
MMDB string `json:"mmdb"`
Obfs string `json:"obfs"`
Auth []byte `json:"auth"`
AuthString string `json:"auth_str"`
ALPN string `json:"alpn"`
ServerName string `json:"server_name"`
Insecure bool `json:"insecure"`
CustomCA string `json:"ca"`
ReceiveWindowConn uint64 `json:"recv_window_conn"`
ReceiveWindow uint64 `json:"recv_window"`
DisableMTUDiscovery bool `json:"disable_mtu_discovery"`
FastOpen bool `json:"fast_open"`
Resolver string `json:"resolver"`
ResolvePreference string `json:"resolve_preference"`
}
func (c *clientConfig) Speed() (uint64, uint64, error) {
var up, down uint64
if len(c.Up) > 0 {
up = stringToBps(c.Up)
if up == 0 {
return 0, 0, errors.New("invalid speed format")
}
} else {
up = uint64(c.UpMbps) * mbpsToBps
}
if len(c.Down) > 0 {
down = stringToBps(c.Down)
if down == 0 {
return 0, 0, errors.New("invalid speed format")
}
} else {
down = uint64(c.DownMbps) * mbpsToBps
}
return up, down, nil
}
func (c *clientConfig) Check() error {
if len(c.SOCKS5.Listen) == 0 && len(c.HTTP.Listen) == 0 && len(c.TUN.Name) == 0 &&
len(c.TCPRelay.Listen) == 0 && len(c.UDPRelay.Listen) == 0 &&
len(c.TCPRelays) == 0 && len(c.UDPRelays) == 0 &&
len(c.TCPTProxy.Listen) == 0 && len(c.UDPTProxy.Listen) == 0 &&
len(c.TCPRedirect.Listen) == 0 {
return errors.New("please enable at least one mode")
}
if c.HandshakeTimeout != 0 && c.HandshakeTimeout < 2 {
return errors.New("invalid handshake timeout")
}
if c.IdleTimeout != 0 && c.IdleTimeout < 4 {
return errors.New("invalid idle timeout")
}
if c.HopInterval != 0 && c.HopInterval < 8 {
return errors.New("invalid hop interval")
}
if c.SOCKS5.Timeout != 0 && c.SOCKS5.Timeout < 4 {
return errors.New("invalid SOCKS5 timeout")
}
if c.HTTP.Timeout != 0 && c.HTTP.Timeout < 4 {
return errors.New("invalid HTTP timeout")
}
if c.TUN.Timeout != 0 && c.TUN.Timeout < 4 {
return errors.New("invalid TUN timeout")
}
if len(c.TCPRelay.Listen) > 0 && len(c.TCPRelay.Remote) == 0 {
return errors.New("missing TCP relay remote address")
}
if len(c.UDPRelay.Listen) > 0 && len(c.UDPRelay.Remote) == 0 {
return errors.New("missing UDP relay remote address")
}
if c.TCPRelay.Timeout != 0 && c.TCPRelay.Timeout < 4 {
return errors.New("invalid TCP relay timeout")
}
if c.UDPRelay.Timeout != 0 && c.UDPRelay.Timeout < 4 {
return errors.New("invalid UDP relay timeout")
}
for _, r := range c.TCPRelays {
if err := r.Check(); err != nil {
return err
}
}
for _, r := range c.UDPRelays {
if err := r.Check(); err != nil {
return err
}
}
if c.TCPTProxy.Timeout != 0 && c.TCPTProxy.Timeout < 4 {
return errors.New("invalid TCP TProxy timeout")
}
if c.UDPTProxy.Timeout != 0 && c.UDPTProxy.Timeout < 4 {
return errors.New("invalid UDP TProxy timeout")
}
if c.TCPRedirect.Timeout != 0 && c.TCPRedirect.Timeout < 4 {
return errors.New("invalid TCP Redirect timeout")
}
if len(c.Server) == 0 {
return errors.New("missing server address")
}
if up, down, err := c.Speed(); err != nil || up < minSpeedBPS || down < minSpeedBPS {
return errors.New("invalid speed")
}
if (c.ReceiveWindowConn != 0 && c.ReceiveWindowConn < 65536) ||
(c.ReceiveWindow != 0 && c.ReceiveWindow < 65536) {
return errors.New("invalid receive window size")
}
if len(c.TCPRelay.Listen) > 0 {
logrus.Warn("'relay_tcp' is deprecated, consider using 'relay_tcps' instead")
}
if len(c.UDPRelay.Listen) > 0 {
logrus.Warn("'relay_udp' is deprecated, consider using 'relay_udps' instead")
}
return nil
}
func (c *clientConfig) Fill() {
if len(c.ALPN) == 0 {
c.ALPN = DefaultALPN
}
if c.ReceiveWindowConn == 0 {
c.ReceiveWindowConn = DefaultStreamReceiveWindow
}
if c.ReceiveWindow == 0 {
c.ReceiveWindow = DefaultConnectionReceiveWindow
}
if len(c.MMDB) == 0 {
c.MMDB = DefaultMMDBFilename
}
if c.IdleTimeout == 0 {
c.IdleTimeout = DefaultClientIdleTimeoutSec
}
if c.HopInterval == 0 {
c.HopInterval = DefaultClientHopIntervalSec
}
}
func (c *clientConfig) String() string {
return fmt.Sprintf("%+v", *c)
}
func stringToBps(s string) uint64 {
if s == "" {
return 0
}
m := rateStringRegexp.FindStringSubmatch(s)
if m == nil {
return 0
}
var n uint64
switch m[2] {
case "K":
n = 1 << 10
case "M":
n = 1 << 20
case "G":
n = 1 << 30
case "T":
n = 1 << 40
default:
n = 1
}
v, _ := strconv.ParseUint(m[1], 10, 64)
n = v * n
if m[3] == "b" {
// Bits, need to convert to bytes
n = n >> 3
}
return n
}

34
app/cmd/config_test.go Normal file
View file

@ -0,0 +1,34 @@
package main
import "testing"
func Test_stringToBps(t *testing.T) {
tests := []struct {
name string
s string
want uint64
}{
{name: "bps 1", s: "8 bps", want: 1},
{name: "bps 2", s: "3 bps", want: 0},
{name: "Bps", s: "9991Bps", want: 9991},
{name: "KBps", s: "10 KBps", want: 10240},
{name: "Kbps", s: "10 Kbps", want: 1280},
{name: "MBps", s: "10 MBps", want: 10485760},
{name: "Mbps", s: "10 Mbps", want: 1310720},
{name: "GBps", s: "10 GBps", want: 10737418240},
{name: "Gbps", s: "10 Gbps", want: 1342177280},
{name: "TBps", s: "10 TBps", want: 10995116277760},
{name: "Tbps", s: "10 Tbps", want: 1374389534720},
{name: "invalid 1", s: "6699E Kbps", want: 0},
{name: "invalid 2", s: "400 Bsp", want: 0},
{name: "invalid 3", s: "9 GBbps", want: 0},
{name: "invalid 4", s: "Mbps", want: 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := stringToBps(tt.s); got != tt.want {
t.Errorf("stringToBps() = %v, want %v", got, tt.want)
}
})
}
}

43
app/cmd/ipmasker.go Normal file
View file

@ -0,0 +1,43 @@
package main
import (
"net"
)
type ipMasker struct {
IPv4Mask net.IPMask
IPv6Mask net.IPMask
}
// Mask masks an address with the configured CIDR.
// addr can be "host:port" or just host.
func (m *ipMasker) Mask(addr string) string {
if m.IPv4Mask == nil && m.IPv6Mask == nil {
return addr
}
host, port, err := net.SplitHostPort(addr)
if err != nil {
// just host
host, port = addr, ""
}
ip := net.ParseIP(host)
if ip == nil {
// not an IP address, return as is
return addr
}
if ip4 := ip.To4(); ip4 != nil && m.IPv4Mask != nil {
// IPv4
host = ip4.Mask(m.IPv4Mask).String()
} else if ip6 := ip.To16(); ip6 != nil && m.IPv6Mask != nil {
// IPv6
host = ip6.Mask(m.IPv6Mask).String()
}
if port != "" {
return net.JoinHostPort(host, port)
} else {
return host
}
}
var defaultIPMasker = &ipMasker{}

95
app/cmd/kploader.go Normal file
View file

@ -0,0 +1,95 @@
package main
import (
"crypto/tls"
"sync"
"github.com/fsnotify/fsnotify"
"github.com/sirupsen/logrus"
)
type keypairLoader struct {
certMu sync.RWMutex
cert *tls.Certificate
certPath string
keyPath string
}
func newKeypairLoader(certPath, keyPath string) (*keypairLoader, error) {
loader := &keypairLoader{
certPath: certPath,
keyPath: keyPath,
}
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
if err != nil {
return nil, err
}
loader.cert = &cert
watcher, err := fsnotify.NewWatcher()
if err != nil {
return nil, err
}
go func() {
for {
select {
case event, ok := <-watcher.Events:
if !ok {
return
}
switch event.Op {
case fsnotify.Create, fsnotify.Write, fsnotify.Rename, fsnotify.Chmod:
logrus.WithFields(logrus.Fields{
"file": event.Name,
}).Info("Keypair change detected, reloading...")
if err := loader.load(); err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
}).Error("Failed to reload keypair")
} else {
logrus.Info("Keypair successfully reloaded")
}
case fsnotify.Remove:
_ = watcher.Add(event.Name) // Workaround for vim
// https://github.com/fsnotify/fsnotify/issues/92
}
case err, ok := <-watcher.Errors:
if !ok {
return
}
logrus.WithFields(logrus.Fields{
"error": err,
}).Error("Failed to watch keypair files for changes")
}
}
}()
err = watcher.Add(certPath)
if err != nil {
_ = watcher.Close()
return nil, err
}
err = watcher.Add(keyPath)
if err != nil {
_ = watcher.Close()
return nil, err
}
return loader, nil
}
func (kpr *keypairLoader) load() error {
cert, err := tls.LoadX509KeyPair(kpr.certPath, kpr.keyPath)
if err != nil {
return err
}
kpr.certMu.Lock()
kpr.cert = &cert
kpr.certMu.Unlock()
return nil
}
func (kpr *keypairLoader) GetCertificateFunc() func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
return func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
kpr.certMu.RLock()
defer kpr.certMu.RUnlock()
return kpr.cert, nil
}
}

208
app/cmd/main.go Normal file
View file

@ -0,0 +1,208 @@
package main
import (
"fmt"
"io/ioutil"
"math/rand"
"net"
"os"
"regexp"
"strings"
"time"
nested "github.com/antonfisher/nested-logrus-formatter"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
const (
logo = `
`
desc = "A TCP/UDP relay & SOCKS5/HTTP proxy tool optimized for poor network environments"
authors = "Aperture Internet Laboratory <https://github.com/apernet>"
)
var (
appVersion = "Unknown"
appCommit = "Unknown"
appDate = "Unknown"
)
var rootCmd = &cobra.Command{
Use: "hysteria",
Long: fmt.Sprintf("%s%s\n\nVersion:\t%s\nBuildDate:\t%s\nCommitHash:\t%s\nAuthors:\t%s", logo, desc, appVersion, appDate, appCommit, authors),
Example: "./hysteria server --config /etc/hysteria.json",
Version: fmt.Sprintf("%s %s %s", appVersion, appDate, appCommit),
PersistentPreRun: func(cmd *cobra.Command, args []string) {
rand.Seed(time.Now().UnixNano())
// log config
logrus.SetOutput(os.Stdout)
if lvl, err := logrus.ParseLevel(viper.GetString("log-level")); err == nil {
logrus.SetLevel(lvl)
} else {
logrus.SetLevel(logrus.DebugLevel)
}
if strings.ToLower(viper.GetString("log-format")) == "json" {
logrus.SetFormatter(&logrus.JSONFormatter{
TimestampFormat: viper.GetString("log-timestamp"),
})
} else {
logrus.SetFormatter(&nested.Formatter{
FieldsOrder: []string{
"version", "url",
"config", "file", "mode", "protocol",
"cert", "key",
"addr", "src", "dst", "session", "action", "interface",
"tcp-sndbuf", "tcp-rcvbuf",
"retry", "interval",
"code", "msg", "error",
},
TimestampFormat: viper.GetString("log-timestamp"),
})
}
// license
if viper.GetBool("license") {
fmt.Printf("%s\n", license)
os.Exit(0)
}
// ip mask config
v4m := viper.GetUint("log-ipv4-mask")
if v4m > 0 && v4m < 32 {
defaultIPMasker.IPv4Mask = net.CIDRMask(int(v4m), 32)
}
v6m := viper.GetUint("log-ipv6-mask")
if v6m > 0 && v6m < 128 {
defaultIPMasker.IPv6Mask = net.CIDRMask(int(v6m), 128)
}
// check update
if !viper.GetBool("no-check") {
go checkUpdate()
}
},
Run: func(cmd *cobra.Command, args []string) {
clientCmd.Run(cmd, args)
},
}
var clientCmd = &cobra.Command{
Use: "client",
Short: "Run as client mode",
Example: "./hysteria client --config /etc/hysteria/client.json",
Run: func(cmd *cobra.Command, args []string) {
cbs, err := ioutil.ReadFile(viper.GetString("config"))
if err != nil {
logrus.WithFields(logrus.Fields{
"file": viper.GetString("config"),
"error": err,
}).Fatal("Failed to read configuration")
}
// client mode
cc, err := parseClientConfig(cbs)
if err != nil {
logrus.WithFields(logrus.Fields{
"file": viper.GetString("config"),
"error": err,
}).Fatal("Failed to parse client configuration")
}
client(cc)
},
}
var serverCmd = &cobra.Command{
Use: "server",
Short: "Run as server mode",
Example: "./hysteria server --config /etc/hysteria/server.json",
Run: func(cmd *cobra.Command, args []string) {
cbs, err := ioutil.ReadFile(viper.GetString("config"))
if err != nil {
logrus.WithFields(logrus.Fields{
"file": viper.GetString("config"),
"error": err,
}).Fatal("Failed to read configuration")
}
// server mode
sc, err := parseServerConfig(cbs)
if err != nil {
logrus.WithFields(logrus.Fields{
"file": viper.GetString("config"),
"error": err,
}).Fatal("Failed to parse server configuration")
}
server(sc)
},
}
// fakeFlags replace the old flag format with the new format(eg: `-config` ->> `--config`)
func fakeFlags() {
var args []string
fr, _ := regexp.Compile(`^-[a-zA-Z]{2,}`)
for _, arg := range os.Args {
if fr.MatchString(arg) {
args = append(args, "-"+arg)
} else {
args = append(args, arg)
}
}
os.Args = args
}
func init() {
// compatible with old flag format
fakeFlags()
// compatible windows double click
cobra.MousetrapHelpText = ""
// disable cmd sorting
cobra.EnableCommandSorting = false
// add global flags
rootCmd.PersistentFlags().StringP("config", "c", "./config.json", "config file")
rootCmd.PersistentFlags().String("mmdb-url", "https://github.com/P3TERX/GeoLite.mmdb/raw/download/GeoLite2-Country.mmdb", "mmdb download url")
rootCmd.PersistentFlags().String("log-level", "debug", "log level")
rootCmd.PersistentFlags().String("log-timestamp", time.RFC3339, "log timestamp format")
rootCmd.PersistentFlags().String("log-format", "txt", "log output format (txt/json)")
rootCmd.PersistentFlags().Uint("log-ipv4-mask", 0, "mask IPv4 addresses in log using a CIDR mask")
rootCmd.PersistentFlags().Uint("log-ipv6-mask", 0, "mask IPv6 addresses in log using a CIDR mask")
rootCmd.PersistentFlags().Bool("no-check", false, "disable update check")
rootCmd.PersistentFlags().Bool("license", false, "show license and exit")
// add to root cmd
rootCmd.AddCommand(clientCmd, serverCmd, completionCmd)
// bind flag
_ = viper.BindPFlag("config", rootCmd.PersistentFlags().Lookup("config"))
_ = viper.BindPFlag("mmdb-url", rootCmd.PersistentFlags().Lookup("mmdb-url"))
_ = viper.BindPFlag("log-level", rootCmd.PersistentFlags().Lookup("log-level"))
_ = viper.BindPFlag("log-timestamp", rootCmd.PersistentFlags().Lookup("log-timestamp"))
_ = viper.BindPFlag("log-format", rootCmd.PersistentFlags().Lookup("log-format"))
_ = viper.BindPFlag("log-ipv4-mask", rootCmd.PersistentFlags().Lookup("log-ipv4-mask"))
_ = viper.BindPFlag("log-ipv6-mask", rootCmd.PersistentFlags().Lookup("log-ipv6-mask"))
_ = viper.BindPFlag("no-check", rootCmd.PersistentFlags().Lookup("no-check"))
_ = viper.BindPFlag("license", rootCmd.PersistentFlags().Lookup("license"))
// bind env
_ = viper.BindEnv("config", "HYSTERIA_CONFIG")
_ = viper.BindEnv("mmdb-url", "HYSTERIA_MMDB_URL")
_ = viper.BindEnv("log-level", "HYSTERIA_LOG_LEVEL", "LOGGING_LEVEL")
_ = viper.BindEnv("log-timestamp", "HYSTERIA_LOG_TIMESTAMP", "LOGGING_TIMESTAMP_FORMAT")
_ = viper.BindEnv("log-format", "HYSTERIA_LOG_FORMAT", "LOGGING_FORMATTER")
_ = viper.BindEnv("log-ipv4-mask", "HYSTERIA_LOG_IPV4_MASK", "LOGGING_IPV4_MASK")
_ = viper.BindEnv("log-ipv6-mask", "HYSTERIA_LOG_IPV6_MASK", "LOGGING_IPV6_MASK")
_ = viper.BindEnv("no-check", "HYSTERIA_NO_CHECK", "HYSTERIA_NO_CHECK_UPDATE")
viper.AutomaticEnv()
}
func main() {
cobra.CheckErr(rootCmd.Execute())
}

49
app/cmd/mmdb.go Normal file
View file

@ -0,0 +1,49 @@
package main
import (
"io"
"net/http"
"os"
"github.com/oschwald/geoip2-golang"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
)
func downloadMMDB(filename string) error {
resp, err := http.Get(viper.GetString("mmdb-url"))
if err != nil {
return err
}
defer resp.Body.Close()
file, err := os.Create(filename)
if err != nil {
return err
}
defer file.Close()
_, err = io.Copy(file, resp.Body)
return err
}
func loadMMDBReader(filename string) (*geoip2.Reader, error) {
if _, err := os.Stat(filename); err != nil {
if os.IsNotExist(err) {
logrus.Info("GeoLite2 database not found, downloading...")
if err := downloadMMDB(filename); err != nil {
return nil, err
}
logrus.WithFields(logrus.Fields{
"file": filename,
}).Info("GeoLite2 database downloaded")
return geoip2.Open(filename)
} else {
// some other error
return nil, err
}
} else {
// file exists, just open it
return geoip2.Open(filename)
}
}

123
app/cmd/resolver.go Normal file
View file

@ -0,0 +1,123 @@
package main
import (
"crypto/tls"
"errors"
"net"
"net/url"
"strings"
"github.com/apernet/hysteria/core/utils"
rdns "github.com/folbricht/routedns"
)
var errInvalidSyntax = errors.New("invalid syntax")
func setResolver(dns string) error {
if net.ParseIP(dns) != nil {
// Just an IP address, treat as UDP 53
dns = "udp://" + net.JoinHostPort(dns, "53")
}
var r rdns.Resolver
if strings.HasPrefix(dns, "udp://") {
// Standard UDP DNS resolver
dns = strings.TrimPrefix(dns, "udp://")
if dns == "" {
return errInvalidSyntax
}
if _, _, err := utils.SplitHostPort(dns); err != nil {
// Append the default DNS port
dns = net.JoinHostPort(dns, "53")
}
client, err := rdns.NewDNSClient("dns-udp", dns, "udp", rdns.DNSClientOptions{})
if err != nil {
return err
}
r = client
} else if strings.HasPrefix(dns, "tcp://") {
// Standard TCP DNS resolver
dns = strings.TrimPrefix(dns, "tcp://")
if dns == "" {
return errInvalidSyntax
}
if _, _, err := utils.SplitHostPort(dns); err != nil {
// Append the default DNS port
dns = net.JoinHostPort(dns, "53")
}
client, err := rdns.NewDNSClient("dns-tcp", dns, "tcp", rdns.DNSClientOptions{})
if err != nil {
return err
}
r = client
} else if strings.HasPrefix(dns, "https://") {
// DoH resolver
if dohURL, err := url.Parse(dns); err != nil {
return err
} else {
// Need to set bootstrap address to avoid loopback DNS lookup
dohIPAddr, err := net.ResolveIPAddr("ip", dohURL.Hostname())
if err != nil {
return err
}
client, err := rdns.NewDoHClient("doh", dns, rdns.DoHClientOptions{
BootstrapAddr: dohIPAddr.String(),
})
if err != nil {
return err
}
r = client
}
} else if strings.HasPrefix(dns, "tls://") {
// DoT resolver
dns = strings.TrimPrefix(dns, "tls://")
if dns == "" {
return errInvalidSyntax
}
dotHost, _, err := utils.SplitHostPort(dns)
if err != nil {
// Append the default DNS port
dns = net.JoinHostPort(dns, "853")
}
// Need to set bootstrap address to avoid loopback DNS lookup
dotIPAddr, err := net.ResolveIPAddr("ip", dotHost)
if err != nil {
return err
}
client, err := rdns.NewDoTClient("dot", dns, rdns.DoTClientOptions{
BootstrapAddr: dotIPAddr.String(),
TLSConfig: new(tls.Config),
})
if err != nil {
return err
}
r = client
} else if strings.HasPrefix(dns, "quic://") {
// DoQ resolver
dns = strings.TrimPrefix(dns, "quic://")
if dns == "" {
return errInvalidSyntax
}
doqHost, _, err := utils.SplitHostPort(dns)
if err != nil {
// Append the default DNS port
dns = net.JoinHostPort(dns, "853")
}
// Need to set bootstrap address to avoid loopback DNS lookup
doqIPAddr, err := net.ResolveIPAddr("ip", doqHost)
if err != nil {
return err
}
client, err := rdns.NewDoQClient("doq", dns, rdns.DoQClientOptions{
BootstrapAddr: doqIPAddr.String(),
})
if err != nil {
return err
}
r = client
} else {
return errInvalidSyntax
}
cache := rdns.NewCache("cache", r, rdns.CacheOptions{})
net.DefaultResolver = rdns.NewNetResolver(cache)
return nil
}

312
app/cmd/server.go Normal file
View file

@ -0,0 +1,312 @@
package main
import (
"crypto/tls"
"io"
"net"
"net/http"
"time"
"github.com/apernet/hysteria/app/auth"
"github.com/apernet/hysteria/core/pktconns"
"github.com/apernet/hysteria/core/acl"
"github.com/apernet/hysteria/core/cs"
"github.com/apernet/hysteria/core/pmtud"
"github.com/apernet/hysteria/core/sockopt"
"github.com/apernet/hysteria/core/transport"
"github.com/lucas-clemente/quic-go"
"github.com/oschwald/geoip2-golang"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/sirupsen/logrus"
"github.com/yosuke-furukawa/json5/encoding/json5"
)
var serverPacketConnFuncFactoryMap = map[string]pktconns.ServerPacketConnFuncFactory{
"": pktconns.NewServerUDPConnFunc,
"udp": pktconns.NewServerUDPConnFunc,
"wechat": pktconns.NewServerWeChatConnFunc,
"wechat-video": pktconns.NewServerWeChatConnFunc,
"faketcp": pktconns.NewServerFakeTCPConnFunc,
}
func server(config *serverConfig) {
logrus.WithField("config", config.String()).Info("Server configuration loaded")
config.Fill() // Fill default values
// Resolver
if len(config.Resolver) > 0 {
err := setResolver(config.Resolver)
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
}).Fatal("Failed to set resolver")
}
}
// Load TLS config
var tlsConfig *tls.Config
if len(config.ACME.Domains) > 0 {
// ACME mode
tc, err := acmeTLSConfig(config.ACME.Domains, config.ACME.Email,
config.ACME.DisableHTTPChallenge, config.ACME.DisableTLSALPNChallenge,
config.ACME.AltHTTPPort, config.ACME.AltTLSALPNPort)
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
}).Fatal("Failed to get a certificate with ACME")
}
tc.NextProtos = []string{config.ALPN}
tc.MinVersion = tls.VersionTLS13
tlsConfig = tc
} else {
// Local cert mode
kpl, err := newKeypairLoader(config.CertFile, config.KeyFile)
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
"cert": config.CertFile,
"key": config.KeyFile,
}).Fatal("Failed to load the certificate")
}
tlsConfig = &tls.Config{
GetCertificate: kpl.GetCertificateFunc(),
NextProtos: []string{config.ALPN},
MinVersion: tls.VersionTLS13,
}
}
// QUIC config
quicConfig := &quic.Config{
InitialStreamReceiveWindow: config.ReceiveWindowConn,
MaxStreamReceiveWindow: config.ReceiveWindowConn,
InitialConnectionReceiveWindow: config.ReceiveWindowClient,
MaxConnectionReceiveWindow: config.ReceiveWindowClient,
MaxIncomingStreams: int64(config.MaxConnClient),
MaxIdleTimeout: ServerMaxIdleTimeoutSec * time.Second,
KeepAlivePeriod: 0, // Keep alive should solely be client's responsibility
DisablePathMTUDiscovery: config.DisableMTUDiscovery,
EnableDatagrams: true,
}
if !quicConfig.DisablePathMTUDiscovery && pmtud.DisablePathMTUDiscovery {
logrus.Info("Path MTU Discovery is not yet supported on this platform")
}
// Auth
var authFunc cs.ConnectFunc
var err error
switch authMode := config.Auth.Mode; authMode {
case "", "none":
if len(config.Obfs) == 0 {
logrus.Warn("Neither authentication nor obfuscation is turned on. " +
"Your server could be used by anyone! Are you sure this is what you want?")
}
authFunc = func(addr net.Addr, auth []byte, sSend uint64, sRecv uint64) (bool, string) {
return true, "Welcome"
}
case "password", "passwords":
authFunc, err = auth.PasswordAuthFunc(config.Auth.Config)
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
}).Fatal("Failed to enable password authentication")
} else {
logrus.Info("Password authentication enabled")
}
case "external":
authFunc, err = auth.ExternalAuthFunc(config.Auth.Config)
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
}).Fatal("Failed to enable external authentication")
} else {
logrus.Info("External authentication enabled")
}
default:
logrus.WithField("mode", config.Auth.Mode).Fatal("Unsupported authentication mode")
}
connectFunc := func(addr net.Addr, auth []byte, sSend uint64, sRecv uint64) (bool, string) {
ok, msg := authFunc(addr, auth, sSend, sRecv)
if !ok {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
"msg": msg,
}).Info("Authentication failed, client rejected")
} else {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
}).Info("Client connected")
}
return ok, msg
}
// Resolve preference
if len(config.ResolvePreference) > 0 {
pref, err := transport.ResolvePreferenceFromString(config.ResolvePreference)
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
}).Fatal("Failed to parse the resolve preference")
}
transport.DefaultServerTransport.ResolvePreference = pref
}
// SOCKS5 outbound
if config.SOCKS5Outbound.Server != "" {
ob, err := transport.NewSOCKS5Client(config.SOCKS5Outbound.Server,
config.SOCKS5Outbound.User, config.SOCKS5Outbound.Password, 10*time.Second)
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
}).Fatal("Failed to initialize SOCKS5 outbound")
}
transport.DefaultServerTransport.SOCKS5Client = ob
}
// Bind outbound
if config.BindOutbound.Device != "" {
iface, err := net.InterfaceByName(config.BindOutbound.Device)
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
}).Fatal("Failed to find the interface")
}
transport.DefaultServerTransport.LocalUDPIntf = iface
sockopt.BindDialer(transport.DefaultServerTransport.Dialer, iface)
}
if config.BindOutbound.Address != "" {
ip := net.ParseIP(config.BindOutbound.Address)
if ip == nil {
logrus.WithFields(logrus.Fields{
"error": err,
}).Fatal("Failed to parse the address")
}
transport.DefaultServerTransport.Dialer.LocalAddr = &net.TCPAddr{IP: ip}
transport.DefaultServerTransport.LocalUDPAddr = &net.UDPAddr{IP: ip}
}
// ACL
var aclEngine *acl.Engine
if len(config.ACL) > 0 {
aclEngine, err = acl.LoadFromFile(config.ACL, func(addr string) (*net.IPAddr, error) {
ipAddr, _, err := transport.DefaultServerTransport.ResolveIPAddr(addr)
return ipAddr, err
},
func() (*geoip2.Reader, error) {
return loadMMDBReader(config.MMDB)
})
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
"file": config.ACL,
}).Fatal("Failed to parse ACL")
}
aclEngine.DefaultAction = acl.ActionDirect
}
// Prometheus
var promReg *prometheus.Registry
if len(config.PrometheusListen) > 0 {
promReg = prometheus.NewRegistry()
go func() {
http.Handle("/metrics", promhttp.HandlerFor(promReg, promhttp.HandlerOpts{}))
err := http.ListenAndServe(config.PrometheusListen, nil)
logrus.WithField("error", err).Fatal("Prometheus HTTP server error")
}()
}
// Packet conn
pktConnFuncFactory := serverPacketConnFuncFactoryMap[config.Protocol]
if pktConnFuncFactory == nil {
logrus.WithField("protocol", config.Protocol).Fatal("Unsupported protocol")
}
pktConnFunc := pktConnFuncFactory(config.Obfs)
pktConn, err := pktConnFunc(config.Listen)
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
"addr": config.Listen,
}).Fatal("Failed to listen on the UDP address")
}
// Server
up, down, _ := config.Speed()
server, err := cs.NewServer(tlsConfig, quicConfig, pktConn,
transport.DefaultServerTransport, up, down, config.DisableUDP, aclEngine,
connectFunc, disconnectFunc, tcpRequestFunc, tcpErrorFunc, udpRequestFunc, udpErrorFunc, promReg)
if err != nil {
logrus.WithField("error", err).Fatal("Failed to initialize server")
}
defer server.Close()
logrus.WithField("addr", config.Listen).Info("Server up and running")
err = server.Serve()
logrus.WithField("error", err).Fatal("Server shutdown")
}
func disconnectFunc(addr net.Addr, auth []byte, err error) {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
"error": err,
}).Info("Client disconnected")
}
func tcpRequestFunc(addr net.Addr, auth []byte, reqAddr string, action acl.Action, arg string) {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
"dst": defaultIPMasker.Mask(reqAddr),
"action": actionToString(action, arg),
}).Debug("TCP request")
}
func tcpErrorFunc(addr net.Addr, auth []byte, reqAddr string, err error) {
if err != io.EOF {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
"dst": defaultIPMasker.Mask(reqAddr),
"error": err,
}).Info("TCP error")
} else {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
"dst": defaultIPMasker.Mask(reqAddr),
}).Debug("TCP EOF")
}
}
func udpRequestFunc(addr net.Addr, auth []byte, sessionID uint32) {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
"session": sessionID,
}).Debug("UDP request")
}
func udpErrorFunc(addr net.Addr, auth []byte, sessionID uint32, err error) {
if err != io.EOF {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
"session": sessionID,
"error": err,
}).Info("UDP error")
} else {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
"session": sessionID,
}).Debug("UDP EOF")
}
}
func actionToString(action acl.Action, arg string) string {
switch action {
case acl.ActionDirect:
return "Direct"
case acl.ActionProxy:
return "Proxy"
case acl.ActionBlock:
return "Block"
case acl.ActionHijack:
return "Hijack to " + arg
default:
return "Unknown"
}
}
func parseServerConfig(cb []byte) (*serverConfig, error) {
var c serverConfig
err := json5.Unmarshal(cb, &c)
if err != nil {
return nil, err
}
return &c, c.Check()
}

49
app/cmd/update.go Normal file
View file

@ -0,0 +1,49 @@
package main
import (
"encoding/json"
"io/ioutil"
"net/http"
"strings"
"time"
"github.com/sirupsen/logrus"
)
const githubAPIURL = "https://api.github.com/repos/apernet/hysteria/releases/latest"
type releaseInfo struct {
URL string `json:"html_url"`
TagName string `json:"tag_name"`
CreatedAt string `json:"created_at"`
PublishedAt string `json:"published_at"`
}
func checkUpdate() {
sv := strings.Split(appVersion, "-")[0]
info, err := fetchLatestRelease()
if err == nil && info.TagName != sv {
logrus.WithFields(logrus.Fields{
"version": info.TagName,
"url": info.URL,
}).Info("New version available")
}
}
func fetchLatestRelease() (*releaseInfo, error) {
hc := &http.Client{
Timeout: time.Second * 20,
}
resp, err := hc.Get(githubAPIURL)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var info releaseInfo
err = json.Unmarshal(body, &info)
return &info, err
}

91
app/http/server.go Normal file
View file

@ -0,0 +1,91 @@
package http
import (
"errors"
"fmt"
"net"
"net/http"
"time"
"github.com/apernet/hysteria/core/transport"
"github.com/apernet/hysteria/core/utils"
"github.com/elazarl/goproxy/ext/auth"
"github.com/apernet/hysteria/core/acl"
"github.com/apernet/hysteria/core/cs"
"github.com/elazarl/goproxy"
)
func NewProxyHTTPServer(hyClient *cs.Client, transport *transport.ClientTransport, idleTimeout time.Duration,
aclEngine *acl.Engine,
basicAuthFunc func(user, password string) bool,
newDialFunc func(reqAddr string, action acl.Action, arg string),
proxyErrorFunc func(reqAddr string, err error),
) (*goproxy.ProxyHttpServer, error) {
proxy := goproxy.NewProxyHttpServer()
proxy.Logger = &nopLogger{}
proxy.NonproxyHandler = http.NotFoundHandler()
proxy.Tr = &http.Transport{
Dial: func(network, addr string) (conn net.Conn, err error) {
defer func() {
if err != nil {
proxyErrorFunc(addr, err)
}
}()
// Parse addr string
host, port, err := utils.SplitHostPort(addr)
if err != nil {
return nil, err
}
// ACL
action, arg := acl.ActionProxy, ""
var ipAddr *net.IPAddr
var resErr error
if aclEngine != nil {
action, arg, _, ipAddr, resErr = aclEngine.ResolveAndMatch(host, port, false)
// Doesn't always matter if the resolution fails, as we may send it through HyClient
}
newDialFunc(addr, action, arg)
// Handle according to the action
switch action {
case acl.ActionDirect:
if resErr != nil {
return nil, resErr
}
return transport.DialTCP(&net.TCPAddr{
IP: ipAddr.IP,
Port: int(port),
Zone: ipAddr.Zone,
})
case acl.ActionProxy:
return hyClient.DialTCP(addr)
case acl.ActionBlock:
return nil, errors.New("blocked by ACL")
case acl.ActionHijack:
hijackIPAddr, err := transport.ResolveIPAddr(arg)
if err != nil {
return nil, err
}
return transport.DialTCP(&net.TCPAddr{
IP: hijackIPAddr.IP,
Port: int(port),
Zone: hijackIPAddr.Zone,
})
default:
return nil, fmt.Errorf("unknown action %d", action)
}
},
IdleConnTimeout: idleTimeout,
// Disable HTTP2 support? ref: https://github.com/elazarl/goproxy/issues/361
}
proxy.ConnectDial = nil
if basicAuthFunc != nil {
auth.ProxyBasic(proxy, "hysteria", basicAuthFunc)
}
return proxy, nil
}
type nopLogger struct{}
func (n *nopLogger) Printf(format string, v ...interface{}) {}

View file

@ -0,0 +1,38 @@
//go:build !386
// +build !386
package redirect
import (
"syscall"
"unsafe"
)
const (
SO_ORIGINAL_DST = 80
IP6T_SO_ORIGINAL_DST = 80
)
type sockAddr struct {
family uint16
port [2]byte // big endian regardless of host byte order
data [24]byte // check sockaddr_in or sockaddr_in6 for more information
}
func getOrigDst(fd uintptr) (*sockAddr, error) {
var addr sockAddr
addrSize := uint32(unsafe.Sizeof(addr))
// try IPv6 first
_, _, err := syscall.Syscall6(syscall.SYS_GETSOCKOPT, fd, syscall.SOL_IPV6, IP6T_SO_ORIGINAL_DST,
uintptr(unsafe.Pointer(&addr)), uintptr(unsafe.Pointer(&addrSize)), 0)
if err != 0 {
// try IPv4
_, _, err = syscall.Syscall6(syscall.SYS_GETSOCKOPT, fd, syscall.SOL_IP, SO_ORIGINAL_DST,
uintptr(unsafe.Pointer(&addr)), uintptr(unsafe.Pointer(&addrSize)), 0)
if err != 0 {
// failed
return nil, err
}
}
return &addr, nil
}

View file

@ -0,0 +1,36 @@
package redirect
import (
"syscall"
"unsafe"
)
const (
SYS_GETSOCKOPT = 15
SO_ORIGINAL_DST = 80
IP6T_SO_ORIGINAL_DST = 80
)
type sockAddr struct {
family uint16
port [2]byte // big endian regardless of host byte order
data [24]byte // check sockaddr_in or sockaddr_in6 for more information
}
func getOrigDst(fd uintptr) (*sockAddr, error) {
var addr sockAddr
addrSize := uint32(unsafe.Sizeof(addr))
// try IPv6 first
_, _, err := syscall.Syscall6(syscall.SYS_SOCKETCALL, SYS_GETSOCKOPT, fd, syscall.SOL_IPV6, IP6T_SO_ORIGINAL_DST,
uintptr(unsafe.Pointer(&addr)), uintptr(unsafe.Pointer(&addrSize)))
if err != 0 {
// try IPv4
_, _, err = syscall.Syscall6(syscall.SYS_SOCKETCALL, SYS_GETSOCKOPT, fd, syscall.SOL_IP, SO_ORIGINAL_DST,
uintptr(unsafe.Pointer(&addr)), uintptr(unsafe.Pointer(&addrSize)))
if err != 0 {
// failed
return nil, err
}
}
return &addr, nil
}

97
app/redirect/tcp_linux.go Normal file
View file

@ -0,0 +1,97 @@
package redirect
import (
"encoding/binary"
"errors"
"net"
"syscall"
"time"
"github.com/apernet/hysteria/core/cs"
"github.com/apernet/hysteria/core/utils"
)
type TCPRedirect struct {
HyClient *cs.Client
ListenAddr *net.TCPAddr
Timeout time.Duration
ConnFunc func(addr, reqAddr net.Addr)
ErrorFunc func(addr, reqAddr net.Addr, err error)
}
func NewTCPRedirect(hyClient *cs.Client, listen string, timeout time.Duration,
connFunc func(addr, reqAddr net.Addr),
errorFunc func(addr, reqAddr net.Addr, err error),
) (*TCPRedirect, error) {
tAddr, err := net.ResolveTCPAddr("tcp", listen)
if err != nil {
return nil, err
}
r := &TCPRedirect{
HyClient: hyClient,
ListenAddr: tAddr,
Timeout: timeout,
ConnFunc: connFunc,
ErrorFunc: errorFunc,
}
return r, nil
}
func (r *TCPRedirect) ListenAndServe() error {
listener, err := net.ListenTCP("tcp", r.ListenAddr)
if err != nil {
return err
}
defer listener.Close()
for {
c, err := listener.Accept()
if err != nil {
return err
}
go func() {
defer c.Close()
dest, err := getDestAddr(c.(*net.TCPConn))
if err != nil || dest.IP.IsLoopback() {
// Silently drop the connection if we failed to get the destination address,
// or if it's a loopback address (not a redirected connection).
return
}
r.ConnFunc(c.RemoteAddr(), dest)
rc, err := r.HyClient.DialTCP(dest.String())
if err != nil {
r.ErrorFunc(c.RemoteAddr(), dest, err)
return
}
defer rc.Close()
err = utils.PipePairWithTimeout(c, rc, r.Timeout)
r.ErrorFunc(c.RemoteAddr(), dest, err)
}()
}
}
func getDestAddr(conn *net.TCPConn) (*net.TCPAddr, error) {
rc, err := conn.SyscallConn()
if err != nil {
return nil, err
}
var addr *sockAddr
var err2 error
err = rc.Control(func(fd uintptr) {
addr, err2 = getOrigDst(fd)
})
if err != nil {
return nil, err
}
if err2 != nil {
return nil, err2
}
switch addr.family {
case syscall.AF_INET:
return &net.TCPAddr{IP: addr.data[:4], Port: int(binary.BigEndian.Uint16(addr.port[:]))}, nil
case syscall.AF_INET6:
return &net.TCPAddr{IP: addr.data[4:20], Port: int(binary.BigEndian.Uint16(addr.port[:]))}, nil
default:
return nil, errors.New("unknown address family")
}
}

25
app/redirect/tcp_stub.go Normal file
View file

@ -0,0 +1,25 @@
//go:build !linux
// +build !linux
package redirect
import (
"errors"
"net"
"time"
"github.com/apernet/hysteria/core/cs"
)
type TCPRedirect struct{}
func NewTCPRedirect(hyClient *cs.Client, listen string, timeout time.Duration,
connFunc func(addr, reqAddr net.Addr),
errorFunc func(addr, reqAddr net.Addr, err error),
) (*TCPRedirect, error) {
return nil, errors.New("not supported on the current system")
}
func (r *TCPRedirect) ListenAndServe() error {
return nil
}

63
app/relay/tcp.go Normal file
View file

@ -0,0 +1,63 @@
package relay
import (
"net"
"time"
"github.com/apernet/hysteria/core/cs"
"github.com/apernet/hysteria/core/utils"
)
type TCPRelay struct {
HyClient *cs.Client
ListenAddr *net.TCPAddr
Remote string
Timeout time.Duration
ConnFunc func(addr net.Addr)
ErrorFunc func(addr net.Addr, err error)
}
func NewTCPRelay(hyClient *cs.Client, listen, remote string, timeout time.Duration,
connFunc func(addr net.Addr), errorFunc func(addr net.Addr, err error),
) (*TCPRelay, error) {
tAddr, err := net.ResolveTCPAddr("tcp", listen)
if err != nil {
return nil, err
}
r := &TCPRelay{
HyClient: hyClient,
ListenAddr: tAddr,
Remote: remote,
Timeout: timeout,
ConnFunc: connFunc,
ErrorFunc: errorFunc,
}
return r, nil
}
func (r *TCPRelay) ListenAndServe() error {
listener, err := net.ListenTCP("tcp", r.ListenAddr)
if err != nil {
return err
}
defer listener.Close()
for {
c, err := listener.AcceptTCP()
if err != nil {
return err
}
go func() {
defer c.Close()
r.ConnFunc(c.RemoteAddr())
rc, err := r.HyClient.DialTCP(r.Remote)
if err != nil {
r.ErrorFunc(c.RemoteAddr(), err)
return
}
defer rc.Close()
err = utils.PipePairWithTimeout(c, rc, r.Timeout)
r.ErrorFunc(c.RemoteAddr(), err)
}()
}
}

124
app/relay/udp.go Normal file
View file

@ -0,0 +1,124 @@
package relay
import (
"errors"
"net"
"sync"
"sync/atomic"
"time"
"github.com/apernet/hysteria/core/cs"
)
const udpBufferSize = 4096
var ErrTimeout = errors.New("inactivity timeout")
type UDPRelay struct {
HyClient *cs.Client
ListenAddr *net.UDPAddr
Remote string
Timeout time.Duration
ConnFunc func(addr net.Addr)
ErrorFunc func(addr net.Addr, err error)
}
func NewUDPRelay(hyClient *cs.Client, listen, remote string, timeout time.Duration,
connFunc func(addr net.Addr), errorFunc func(addr net.Addr, err error),
) (*UDPRelay, error) {
uAddr, err := net.ResolveUDPAddr("udp", listen)
if err != nil {
return nil, err
}
r := &UDPRelay{
HyClient: hyClient,
ListenAddr: uAddr,
Remote: remote,
Timeout: timeout,
ConnFunc: connFunc,
ErrorFunc: errorFunc,
}
if timeout == 0 {
r.Timeout = 1 * time.Minute
}
return r, nil
}
type connEntry struct {
HyConn cs.HyUDPConn
Deadline atomic.Value
}
func (r *UDPRelay) ListenAndServe() error {
conn, err := net.ListenUDP("udp", r.ListenAddr)
if err != nil {
return err
}
defer conn.Close()
// src <-> HyClient HyUDPConn
connMap := make(map[string]*connEntry)
var connMapMutex sync.RWMutex
// Read loop
buf := make([]byte, udpBufferSize)
for {
n, rAddr, err := conn.ReadFromUDP(buf)
if n > 0 {
connMapMutex.RLock()
entry := connMap[rAddr.String()]
connMapMutex.RUnlock()
if entry != nil {
// Existing conn
entry.Deadline.Store(time.Now().Add(r.Timeout))
_ = entry.HyConn.WriteTo(buf[:n], r.Remote)
} else {
// New
r.ConnFunc(rAddr)
hyConn, err := r.HyClient.DialUDP()
if err != nil {
r.ErrorFunc(rAddr, err)
} else {
// Add it to the map
entry := &connEntry{HyConn: hyConn}
entry.Deadline.Store(time.Now().Add(r.Timeout))
connMapMutex.Lock()
connMap[rAddr.String()] = entry
connMapMutex.Unlock()
// Start remote to local
go func() {
for {
bs, _, err := hyConn.ReadFrom()
if err != nil {
break
}
entry.Deadline.Store(time.Now().Add(r.Timeout))
_, _ = conn.WriteToUDP(bs, rAddr)
}
}()
// Timeout cleanup routine
go func() {
for {
ttl := entry.Deadline.Load().(time.Time).Sub(time.Now())
if ttl <= 0 {
// Time to die
connMapMutex.Lock()
_ = hyConn.Close()
delete(connMap, rAddr.String())
connMapMutex.Unlock()
r.ErrorFunc(rAddr, ErrTimeout)
return
} else {
time.Sleep(ttl)
}
}
}()
// Send the packet
_ = hyConn.WriteTo(buf[:n], r.Remote)
}
}
}
if err != nil {
return err
}
}
}

442
app/socks5/server.go Normal file
View file

@ -0,0 +1,442 @@
package socks5
import (
"encoding/binary"
"errors"
"fmt"
"strconv"
"github.com/apernet/hysteria/core/acl"
"github.com/apernet/hysteria/core/cs"
"github.com/apernet/hysteria/core/transport"
"github.com/apernet/hysteria/core/utils"
)
import (
"net"
"time"
"github.com/txthinking/socks5"
)
const udpBufferSize = 4096
var (
ErrUnsupportedCmd = errors.New("unsupported command")
ErrUserPassAuth = errors.New("invalid username or password")
)
type Server struct {
HyClient *cs.Client
Transport *transport.ClientTransport
AuthFunc func(username, password string) bool
Method byte
TCPAddr *net.TCPAddr
TCPTimeout time.Duration
ACLEngine *acl.Engine
DisableUDP bool
TCPRequestFunc func(addr net.Addr, reqAddr string, action acl.Action, arg string)
TCPErrorFunc func(addr net.Addr, reqAddr string, err error)
UDPAssociateFunc func(addr net.Addr)
UDPErrorFunc func(addr net.Addr, err error)
tcpListener *net.TCPListener
}
func NewServer(hyClient *cs.Client, transport *transport.ClientTransport, addr string,
authFunc func(username, password string) bool, tcpTimeout time.Duration,
aclEngine *acl.Engine, disableUDP bool,
tcpReqFunc func(addr net.Addr, reqAddr string, action acl.Action, arg string),
tcpErrorFunc func(addr net.Addr, reqAddr string, err error),
udpAssocFunc func(addr net.Addr), udpErrorFunc func(addr net.Addr, err error),
) (*Server, error) {
tAddr, err := net.ResolveTCPAddr("tcp", addr)
if err != nil {
return nil, err
}
m := socks5.MethodNone
if authFunc != nil {
m = socks5.MethodUsernamePassword
}
s := &Server{
HyClient: hyClient,
Transport: transport,
AuthFunc: authFunc,
Method: m,
TCPAddr: tAddr,
TCPTimeout: tcpTimeout,
ACLEngine: aclEngine,
DisableUDP: disableUDP,
TCPRequestFunc: tcpReqFunc,
TCPErrorFunc: tcpErrorFunc,
UDPAssociateFunc: udpAssocFunc,
UDPErrorFunc: udpErrorFunc,
}
return s, nil
}
func (s *Server) negotiate(c *net.TCPConn) error {
rq, err := socks5.NewNegotiationRequestFrom(c)
if err != nil {
return err
}
var got bool
var m byte
for _, m = range rq.Methods {
if m == s.Method {
got = true
}
}
if !got {
rp := socks5.NewNegotiationReply(socks5.MethodUnsupportAll)
if _, err := rp.WriteTo(c); err != nil {
return err
}
}
rp := socks5.NewNegotiationReply(s.Method)
if _, err := rp.WriteTo(c); err != nil {
return err
}
if s.Method == socks5.MethodUsernamePassword {
urq, err := socks5.NewUserPassNegotiationRequestFrom(c)
if err != nil {
return err
}
if !s.AuthFunc(string(urq.Uname), string(urq.Passwd)) {
urp := socks5.NewUserPassNegotiationReply(socks5.UserPassStatusFailure)
if _, err := urp.WriteTo(c); err != nil {
return err
}
return ErrUserPassAuth
}
urp := socks5.NewUserPassNegotiationReply(socks5.UserPassStatusSuccess)
if _, err := urp.WriteTo(c); err != nil {
return err
}
}
return nil
}
func (s *Server) ListenAndServe() error {
var err error
s.tcpListener, err = net.ListenTCP("tcp", s.TCPAddr)
if err != nil {
return err
}
defer s.tcpListener.Close()
for {
c, err := s.tcpListener.AcceptTCP()
if err != nil {
return err
}
go func() {
defer c.Close()
if s.TCPTimeout != 0 {
if err := c.SetDeadline(time.Now().Add(s.TCPTimeout)); err != nil {
return
}
}
if err := s.negotiate(c); err != nil {
return
}
r, err := socks5.NewRequestFrom(c)
if err != nil {
return
}
_ = s.handle(c, r)
}()
}
}
func (s *Server) handle(c *net.TCPConn, r *socks5.Request) error {
if r.Cmd == socks5.CmdConnect {
// TCP
return s.handleTCP(c, r)
} else if r.Cmd == socks5.CmdUDP {
// UDP
if !s.DisableUDP {
return s.handleUDP(c, r)
} else {
_ = sendReply(c, socks5.RepCommandNotSupported)
return ErrUnsupportedCmd
}
} else {
_ = sendReply(c, socks5.RepCommandNotSupported)
return ErrUnsupportedCmd
}
}
func (s *Server) handleTCP(c *net.TCPConn, r *socks5.Request) error {
host, port, addr := parseRequestAddress(r)
action, arg := acl.ActionProxy, ""
var ipAddr *net.IPAddr
var resErr error
if s.ACLEngine != nil {
action, arg, _, ipAddr, resErr = s.ACLEngine.ResolveAndMatch(host, port, false)
// Doesn't always matter if the resolution fails, as we may send it through HyClient
}
s.TCPRequestFunc(c.RemoteAddr(), addr, action, arg)
var closeErr error
defer func() {
s.TCPErrorFunc(c.RemoteAddr(), addr, closeErr)
}()
// Handle according to the action
switch action {
case acl.ActionDirect:
if resErr != nil {
_ = sendReply(c, socks5.RepHostUnreachable)
closeErr = resErr
return resErr
}
rc, err := s.Transport.DialTCP(&net.TCPAddr{
IP: ipAddr.IP,
Port: int(port),
Zone: ipAddr.Zone,
})
if err != nil {
_ = sendReply(c, socks5.RepHostUnreachable)
closeErr = err
return err
}
defer rc.Close()
_ = sendReply(c, socks5.RepSuccess)
closeErr = utils.PipePairWithTimeout(c, rc, s.TCPTimeout)
return nil
case acl.ActionProxy:
rc, err := s.HyClient.DialTCP(addr)
if err != nil {
_ = sendReply(c, socks5.RepHostUnreachable)
closeErr = err
return err
}
defer rc.Close()
_ = sendReply(c, socks5.RepSuccess)
closeErr = utils.PipePairWithTimeout(c, rc, s.TCPTimeout)
return nil
case acl.ActionBlock:
_ = sendReply(c, socks5.RepHostUnreachable)
closeErr = errors.New("blocked in ACL")
return nil
case acl.ActionHijack:
hijackIPAddr, err := s.Transport.ResolveIPAddr(arg)
if err != nil {
_ = sendReply(c, socks5.RepHostUnreachable)
closeErr = err
return err
}
rc, err := s.Transport.DialTCP(&net.TCPAddr{
IP: hijackIPAddr.IP,
Port: int(port),
Zone: hijackIPAddr.Zone,
})
if err != nil {
_ = sendReply(c, socks5.RepHostUnreachable)
closeErr = err
return err
}
defer rc.Close()
_ = sendReply(c, socks5.RepSuccess)
closeErr = utils.PipePairWithTimeout(c, rc, s.TCPTimeout)
return nil
default:
_ = sendReply(c, socks5.RepServerFailure)
closeErr = fmt.Errorf("unknown action %d", action)
return nil
}
}
func (s *Server) handleUDP(c *net.TCPConn, r *socks5.Request) error {
s.UDPAssociateFunc(c.RemoteAddr())
var closeErr error
defer func() {
s.UDPErrorFunc(c.RemoteAddr(), closeErr)
}()
// Start local UDP server
udpConn, err := net.ListenUDP("udp", &net.UDPAddr{
IP: s.TCPAddr.IP,
Zone: s.TCPAddr.Zone,
})
if err != nil {
_ = sendReply(c, socks5.RepServerFailure)
closeErr = err
return err
}
defer udpConn.Close()
// Local UDP relay conn for ACL Direct
var localRelayConn *net.UDPConn
if s.ACLEngine != nil {
localRelayConn, err = s.Transport.ListenUDP()
if err != nil {
_ = sendReply(c, socks5.RepServerFailure)
closeErr = err
return err
}
defer localRelayConn.Close()
}
// HyClient UDP session
hyUDP, err := s.HyClient.DialUDP()
if err != nil {
_ = sendReply(c, socks5.RepServerFailure)
closeErr = err
return err
}
defer hyUDP.Close()
// Send UDP server addr to the client
// Same IP as TCP but a different port
tcpLocalAddr := c.LocalAddr().(*net.TCPAddr)
var atyp byte
var addr, port []byte
if ip4 := tcpLocalAddr.IP.To4(); ip4 != nil {
atyp = socks5.ATYPIPv4
addr = ip4
} else if ip6 := tcpLocalAddr.IP.To16(); ip6 != nil {
atyp = socks5.ATYPIPv6
addr = ip6
} else {
_ = sendReply(c, socks5.RepServerFailure)
closeErr = errors.New("invalid local addr")
return closeErr
}
port = make([]byte, 2)
binary.BigEndian.PutUint16(port, uint16(udpConn.LocalAddr().(*net.UDPAddr).Port))
_, _ = socks5.NewReply(socks5.RepSuccess, atyp, addr, port).WriteTo(c)
// Let UDP server do its job, we hold the TCP connection here
go s.udpServer(udpConn, localRelayConn, hyUDP)
if s.TCPTimeout != 0 {
// Disable TCP timeout for UDP holder
_ = c.SetDeadline(time.Time{})
}
buf := make([]byte, 1024)
for {
_, err := c.Read(buf)
if err != nil {
closeErr = err
break
}
}
// As the TCP connection closes, so does the UDP server & HyClient session
return nil
}
func (s *Server) udpServer(clientConn *net.UDPConn, localRelayConn *net.UDPConn, hyUDP cs.HyUDPConn) {
var clientAddr *net.UDPAddr
buf := make([]byte, udpBufferSize)
// Local to remote
for {
n, cAddr, err := clientConn.ReadFromUDP(buf)
if err != nil {
break
}
d, err := socks5.NewDatagramFromBytes(buf[:n])
if err != nil || d.Frag != 0 {
// Ignore bad packets
continue
}
if clientAddr == nil {
// Whoever sends the first valid packet is our client
clientAddr = cAddr
// Start remote to local
go func() {
for {
bs, from, err := hyUDP.ReadFrom()
if err != nil {
break
}
atyp, addr, port, err := socks5.ParseAddress(from)
if err != nil {
continue
}
d := socks5.NewDatagram(atyp, addr, port, bs)
_, _ = clientConn.WriteToUDP(d.Bytes(), clientAddr)
}
}()
if localRelayConn != nil {
go func() {
buf := make([]byte, udpBufferSize)
for {
n, from, err := localRelayConn.ReadFrom(buf)
if n > 0 {
atyp, addr, port, err := socks5.ParseAddress(from.String())
if err != nil {
continue
}
d := socks5.NewDatagram(atyp, addr, port, buf[:n])
_, _ = clientConn.WriteToUDP(d.Bytes(), clientAddr)
}
if err != nil {
break
}
}
}()
}
} else if cAddr.String() != clientAddr.String() {
// Not our client, bye
continue
}
host, port, addr := parseDatagramRequestAddress(d)
action, arg := acl.ActionProxy, ""
var ipAddr *net.IPAddr
var resErr error
if s.ACLEngine != nil && localRelayConn != nil {
action, arg, _, ipAddr, resErr = s.ACLEngine.ResolveAndMatch(host, port, true)
// Doesn't always matter if the resolution fails, as we may send it through HyClient
}
// Handle according to the action
switch action {
case acl.ActionDirect:
if resErr != nil {
return
}
_, _ = localRelayConn.WriteToUDP(d.Data, &net.UDPAddr{
IP: ipAddr.IP,
Port: int(port),
Zone: ipAddr.Zone,
})
case acl.ActionProxy:
_ = hyUDP.WriteTo(d.Data, addr)
case acl.ActionBlock:
// Do nothing
case acl.ActionHijack:
hijackIPAddr, err := s.Transport.ResolveIPAddr(arg)
if err == nil {
_, _ = localRelayConn.WriteToUDP(d.Data, &net.UDPAddr{
IP: hijackIPAddr.IP,
Port: int(port),
Zone: hijackIPAddr.Zone,
})
}
default:
// Do nothing
}
}
}
func sendReply(conn *net.TCPConn, rep byte) error {
p := socks5.NewReply(rep, socks5.ATYPIPv4, []byte{0x00, 0x00, 0x00, 0x00}, []byte{0x00, 0x00})
_, err := p.WriteTo(conn)
return err
}
func parseRequestAddress(r *socks5.Request) (host string, port uint16, addr string) {
p := binary.BigEndian.Uint16(r.DstPort)
if r.Atyp == socks5.ATYPDomain {
d := string(r.DstAddr[1:])
return d, p, net.JoinHostPort(d, strconv.Itoa(int(p)))
} else {
ipStr := net.IP(r.DstAddr).String()
return ipStr, p, net.JoinHostPort(ipStr, strconv.Itoa(int(p)))
}
}
func parseDatagramRequestAddress(r *socks5.Datagram) (host string, port uint16, addr string) {
p := binary.BigEndian.Uint16(r.DstPort)
if r.Atyp == socks5.ATYPDomain {
d := string(r.DstAddr[1:])
return d, p, net.JoinHostPort(d, strconv.Itoa(int(p)))
} else {
ipStr := net.IP(r.DstAddr).String()
return ipStr, p, net.JoinHostPort(ipStr, strconv.Itoa(int(p)))
}
}

66
app/tproxy/tcp_linux.go Normal file
View file

@ -0,0 +1,66 @@
package tproxy
import (
"net"
"time"
"github.com/LiamHaworth/go-tproxy"
"github.com/apernet/hysteria/core/cs"
"github.com/apernet/hysteria/core/utils"
)
type TCPTProxy struct {
HyClient *cs.Client
ListenAddr *net.TCPAddr
Timeout time.Duration
ConnFunc func(addr, reqAddr net.Addr)
ErrorFunc func(addr, reqAddr net.Addr, err error)
}
func NewTCPTProxy(hyClient *cs.Client, listen string, timeout time.Duration,
connFunc func(addr, reqAddr net.Addr),
errorFunc func(addr, reqAddr net.Addr, err error),
) (*TCPTProxy, error) {
tAddr, err := net.ResolveTCPAddr("tcp", listen)
if err != nil {
return nil, err
}
r := &TCPTProxy{
HyClient: hyClient,
ListenAddr: tAddr,
Timeout: timeout,
ConnFunc: connFunc,
ErrorFunc: errorFunc,
}
return r, nil
}
func (r *TCPTProxy) ListenAndServe() error {
listener, err := tproxy.ListenTCP("tcp", r.ListenAddr)
if err != nil {
return err
}
defer listener.Close()
for {
c, err := listener.Accept()
if err != nil {
return err
}
go func() {
defer c.Close()
// Under TPROXY mode, we are effectively acting as the remote server
// So our LocalAddr is actually the target to which the user is trying to connect
// and our RemoteAddr is the local address where the user initiates the connection
r.ConnFunc(c.RemoteAddr(), c.LocalAddr())
rc, err := r.HyClient.DialTCP(c.LocalAddr().String())
if err != nil {
r.ErrorFunc(c.RemoteAddr(), c.LocalAddr(), err)
return
}
defer rc.Close()
err = utils.PipePairWithTimeout(c, rc, r.Timeout)
r.ErrorFunc(c.RemoteAddr(), c.LocalAddr(), err)
}()
}
}

25
app/tproxy/tcp_stub.go Normal file
View file

@ -0,0 +1,25 @@
//go:build !linux
// +build !linux
package tproxy
import (
"errors"
"net"
"time"
"github.com/apernet/hysteria/core/cs"
)
type TCPTProxy struct{}
func NewTCPTProxy(hyClient *cs.Client, listen string, timeout time.Duration,
connFunc func(addr, reqAddr net.Addr),
errorFunc func(addr, reqAddr net.Addr, err error),
) (*TCPTProxy, error) {
return nil, errors.New("not supported on the current system")
}
func (r *TCPTProxy) ListenAndServe() error {
return nil
}

115
app/tproxy/udp_linux.go Normal file
View file

@ -0,0 +1,115 @@
package tproxy
import (
"net"
"time"
"github.com/LiamHaworth/go-tproxy"
"github.com/apernet/hysteria/core/cs"
)
const udpBufferSize = 4096
type UDPTProxy struct {
HyClient *cs.Client
ListenAddr *net.UDPAddr
Timeout time.Duration
ConnFunc func(addr, reqAddr net.Addr)
ErrorFunc func(addr, reqAddr net.Addr, err error)
}
func NewUDPTProxy(hyClient *cs.Client, listen string, timeout time.Duration,
connFunc func(addr, reqAddr net.Addr),
errorFunc func(addr, reqAddr net.Addr, err error),
) (*UDPTProxy, error) {
uAddr, err := net.ResolveUDPAddr("udp", listen)
if err != nil {
return nil, err
}
r := &UDPTProxy{
HyClient: hyClient,
ListenAddr: uAddr,
Timeout: timeout,
ConnFunc: connFunc,
ErrorFunc: errorFunc,
}
if timeout == 0 {
r.Timeout = 1 * time.Minute
}
return r, nil
}
func (r *UDPTProxy) ListenAndServe() error {
conn, err := tproxy.ListenUDP("udp", r.ListenAddr)
if err != nil {
return err
}
defer conn.Close()
// Read loop
buf := make([]byte, udpBufferSize)
for {
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
if n > 0 {
r.ConnFunc(srcAddr, dstAddr)
localConn, err := tproxy.DialUDP("udp", dstAddr, srcAddr)
if err != nil {
r.ErrorFunc(srcAddr, dstAddr, err)
continue
}
hyConn, err := r.HyClient.DialUDP()
if err != nil {
r.ErrorFunc(srcAddr, dstAddr, err)
_ = localConn.Close()
continue
}
_ = hyConn.WriteTo(buf[:n], dstAddr.String())
errChan := make(chan error, 2)
// Start remote to local
go func() {
for {
bs, _, err := hyConn.ReadFrom()
if err != nil {
errChan <- err
return
}
_, err = localConn.Write(bs)
if err != nil {
errChan <- err
return
}
_ = localConn.SetDeadline(time.Now().Add(r.Timeout))
}
}()
// Start local to remote
go func() {
for {
_ = localConn.SetDeadline(time.Now().Add(r.Timeout))
n, err := localConn.Read(buf)
if n > 0 {
err := hyConn.WriteTo(buf[:n], dstAddr.String())
if err != nil {
errChan <- err
return
}
}
if err != nil {
errChan <- err
return
}
}
}()
// Error cleanup routine
go func() {
err := <-errChan
_ = localConn.Close()
_ = hyConn.Close()
r.ErrorFunc(srcAddr, dstAddr, err)
}()
}
if err != nil {
return err
}
}
}

26
app/tproxy/udp_stub.go Normal file
View file

@ -0,0 +1,26 @@
//go:build !linux
// +build !linux
package tproxy
import (
"errors"
"net"
"time"
"github.com/apernet/hysteria/core/cs"
)
var ErrTimeout = errors.New("inactivity timeout")
type UDPTProxy struct{}
func NewUDPTProxy(hyClient *cs.Client, listen string, timeout time.Duration,
connFunc func(addr, reqAddr net.Addr), errorFunc func(addr, reqAddr net.Addr, err error),
) (*UDPTProxy, error) {
return nil, errors.New("not supported on the current system")
}
func (r *UDPTProxy) ListenAndServe() error {
return nil
}

160
app/tun/server.go Normal file
View file

@ -0,0 +1,160 @@
//go:build gpl
// +build gpl
package tun
import (
"fmt"
"net"
"os"
"os/signal"
"strconv"
"syscall"
"time"
"github.com/xjasonlyu/tun2socks/v2/core/option"
"github.com/apernet/hysteria/core/cs"
"github.com/sirupsen/logrus"
t2score "github.com/xjasonlyu/tun2socks/v2/core"
"github.com/xjasonlyu/tun2socks/v2/core/adapter"
"github.com/xjasonlyu/tun2socks/v2/core/device"
"github.com/xjasonlyu/tun2socks/v2/core/device/fdbased"
"github.com/xjasonlyu/tun2socks/v2/core/device/tun"
"gvisor.dev/gvisor/pkg/tcpip/stack"
)
var _ adapter.TransportHandler = (*Server)(nil)
type Server struct {
HyClient *cs.Client
Timeout time.Duration
DeviceInfo DeviceInfo
RequestFunc func(addr net.Addr, reqAddr string)
ErrorFunc func(addr net.Addr, reqAddr string, err error)
}
const (
MTU = 1500
)
const (
DeviceTypeFd = iota
DeviceTypeName
)
type DeviceInfo struct {
Type int
Fd int
Name string
MTU uint32
TCPSendBufferSize int
TCPReceiveBufferSize int
TCPModerateReceiveBuffer bool
}
func (d *DeviceInfo) Open() (dev device.Device, err error) {
switch d.Type {
case DeviceTypeFd:
dev, err = fdbased.Open(strconv.Itoa(d.Fd), d.MTU)
case DeviceTypeName:
dev, err = tun.Open(d.Name, d.MTU)
default:
err = fmt.Errorf("unknown device type: %d", d.Type)
}
return
}
func NewServerWithTunFd(hyClient *cs.Client, timeout time.Duration, tunFd int, mtu uint32,
tcpSendBufferSize, tcpReceiveBufferSize int, tcpModerateReceiveBuffer bool,
) (*Server, error) {
if mtu == 0 {
mtu = MTU
}
s := &Server{
HyClient: hyClient,
Timeout: timeout,
DeviceInfo: DeviceInfo{
Type: DeviceTypeFd,
Fd: tunFd,
MTU: mtu,
TCPSendBufferSize: tcpSendBufferSize,
TCPReceiveBufferSize: tcpReceiveBufferSize,
TCPModerateReceiveBuffer: tcpModerateReceiveBuffer,
},
}
return s, nil
}
func NewServer(hyClient *cs.Client, timeout time.Duration, name string, mtu uint32,
tcpSendBufferSize, tcpReceiveBufferSize int, tcpModerateReceiveBuffer bool,
) (*Server, error) {
if mtu == 0 {
mtu = MTU
}
s := &Server{
HyClient: hyClient,
Timeout: timeout,
DeviceInfo: DeviceInfo{
Type: DeviceTypeName,
Name: name,
MTU: mtu,
TCPSendBufferSize: tcpSendBufferSize,
TCPReceiveBufferSize: tcpReceiveBufferSize,
TCPModerateReceiveBuffer: tcpModerateReceiveBuffer,
},
}
return s, nil
}
func (s *Server) ListenAndServe() error {
var dev device.Device
var st *stack.Stack
defer func() {
if dev != nil {
_ = dev.Close()
}
if st != nil {
st.Close()
st.Wait()
}
}()
dev, err := s.DeviceInfo.Open()
if err != nil {
return err
}
var opts []option.Option
if s.DeviceInfo.TCPSendBufferSize > 0 {
opts = append(opts, option.WithTCPSendBufferSize(s.DeviceInfo.TCPSendBufferSize))
}
if s.DeviceInfo.TCPReceiveBufferSize > 0 {
opts = append(opts, option.WithTCPReceiveBufferSize(s.DeviceInfo.TCPReceiveBufferSize))
}
if s.DeviceInfo.TCPModerateReceiveBuffer {
opts = append(opts, option.WithTCPModerateReceiveBuffer(s.DeviceInfo.TCPModerateReceiveBuffer))
}
t2sconf := t2score.Config{
LinkEndpoint: dev,
TransportHandler: s,
PrintFunc: func(format string, v ...interface{}) {
logrus.Infof(format, v...)
},
Options: opts,
}
st, err = t2score.CreateStack(&t2sconf)
if err != nil {
return err
}
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
return nil
}

48
app/tun/tcp.go Normal file
View file

@ -0,0 +1,48 @@
//go:build gpl
// +build gpl
package tun
import (
"net"
"github.com/apernet/hysteria/core/utils"
"github.com/xjasonlyu/tun2socks/v2/core/adapter"
)
func (s *Server) HandleTCP(localConn adapter.TCPConn) {
go s.handleTCPConn(localConn)
}
func (s *Server) handleTCPConn(localConn adapter.TCPConn) {
defer localConn.Close()
id := localConn.ID()
remoteAddr := net.TCPAddr{
IP: net.IP(id.LocalAddress),
Port: int(id.LocalPort),
}
localAddr := net.TCPAddr{
IP: net.IP(id.RemoteAddress),
Port: int(id.RemotePort),
}
if s.RequestFunc != nil {
s.RequestFunc(&localAddr, remoteAddr.String())
}
var err error
defer func() {
if s.ErrorFunc != nil && err != nil {
s.ErrorFunc(&localAddr, remoteAddr.String(), err)
}
}()
rc, err := s.HyClient.DialTCP(remoteAddr.String())
if err != nil {
return
}
defer rc.Close()
err = utils.PipePairWithTimeout(localConn, rc, s.Timeout)
}

114
app/tun/udp.go Normal file
View file

@ -0,0 +1,114 @@
//go:build gpl
// +build gpl
package tun
import (
"fmt"
"net"
"strconv"
"time"
"github.com/apernet/hysteria/core/cs"
"github.com/xjasonlyu/tun2socks/v2/core/adapter"
)
const udpBufferSize = 4096
func (s *Server) HandleUDP(conn adapter.UDPConn) {
go s.handleUDPConn(conn)
}
func (s *Server) handleUDPConn(conn adapter.UDPConn) {
defer conn.Close()
id := conn.ID()
remoteAddr := net.UDPAddr{
IP: net.IP(id.LocalAddress),
Port: int(id.LocalPort),
}
localAddr := net.UDPAddr{
IP: net.IP(id.RemoteAddress),
Port: int(id.RemotePort),
}
if s.RequestFunc != nil {
s.RequestFunc(&localAddr, remoteAddr.String())
}
var err error
defer func() {
if s.ErrorFunc != nil && err != nil {
s.ErrorFunc(&localAddr, remoteAddr.String(), err)
}
}()
rc, err := s.HyClient.DialUDP()
if err != nil {
return
}
defer rc.Close()
err = s.relayUDP(conn, rc, &remoteAddr, s.Timeout)
}
func (s *Server) relayUDP(lc adapter.UDPConn, rc cs.HyUDPConn, to *net.UDPAddr, timeout time.Duration) (err error) {
errChan := make(chan error, 2)
// local => remote
go func() {
buf := make([]byte, udpBufferSize)
for {
if timeout != 0 {
_ = lc.SetDeadline(time.Now().Add(timeout))
n, err := lc.Read(buf)
if n > 0 {
err = rc.WriteTo(buf[:n], to.String())
if err != nil {
errChan <- err
return
}
}
if err != nil {
errChan <- err
return
}
}
}
}()
// remote => local
go func() {
for {
pkt, addr, err := rc.ReadFrom()
if err != nil {
errChan <- err
return
}
if pkt != nil {
host, portStr, err := net.SplitHostPort(addr)
if err != nil {
errChan <- err
return
}
port, err := strconv.Atoi(portStr)
if err != nil {
errChan <- fmt.Errorf("cannot parse as port: %s", portStr)
return
}
// adapter.UDPConn doesn't support WriteFrom() yet,
// so we check the src address and behavior like a symmetric NAT
if !to.IP.Equal(net.ParseIP(host)) || to.Port != port {
// drop the packet silently
continue
}
_, err = lc.Write(pkt)
if err != nil {
errChan <- err
return
}
}
}
}()
return <-errChan
}