Skip to content

Commit 17e326c

Browse files
committed
Add PPP over Hysteria2
1 parent 26a03b0 commit 17e326c

91 files changed

Lines changed: 25099 additions & 60 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/cmd/client.go

Lines changed: 107 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import (
3737
"github.com/apernet/hysteria/app/v2/internal/url"
3838
"github.com/apernet/hysteria/app/v2/internal/utils"
3939
"github.com/apernet/hysteria/core/v2/client"
40+
coreErrs "github.com/apernet/hysteria/core/v2/errors"
4041
"github.com/apernet/hysteria/extras/v2/correctnet"
4142
"github.com/apernet/hysteria/extras/v2/obfs"
4243
"github.com/apernet/hysteria/extras/v2/realm"
@@ -90,6 +91,28 @@ type clientConfig struct {
9091
UDPTProxy *udpTProxyConfig `mapstructure:"udpTProxy"`
9192
TCPRedirect *tcpRedirectConfig `mapstructure:"tcpRedirect"`
9293
TUN *tunConfig `mapstructure:"tun"`
94+
PPP *pppConfig `mapstructure:"ppp"`
95+
}
96+
97+
type pppSSTPConfig struct {
98+
BinaryPath string `mapstructure:"binaryPath"`
99+
Listen string `mapstructure:"listen"`
100+
CertDir string `mapstructure:"certDir"`
101+
Endpoint string `mapstructure:"endpoint"`
102+
User string `mapstructure:"user"`
103+
Password string `mapstructure:"password"`
104+
MSSClamp *int `mapstructure:"mssClamp"` // nil=auto, 0=off, >0=forced
105+
ServerRoute *bool `mapstructure:"serverRoute"`
106+
LogLevel string `mapstructure:"logLevel"`
107+
}
108+
109+
type pppConfig struct {
110+
Mode string `mapstructure:"mode"`
111+
MTU uint32 `mapstructure:"mtu"`
112+
PPPDPath string `mapstructure:"pppdPath"`
113+
PPPDArgs []string `mapstructure:"pppdArgs"`
114+
DataStreams int `mapstructure:"dataStreams"`
115+
SSTP *pppSSTPConfig `mapstructure:"sstp"`
93116
}
94117

95118
type clientConfigRealm struct {
@@ -618,6 +641,9 @@ func (c *clientConfig) Config() (*client.Config, error) {
618641
return nil, err
619642
}
620643
}
644+
if c.PPP != nil && c.PPP.DataStreams == 0 {
645+
hyConfig.PPPMode = true
646+
}
621647
return hyConfig, nil
622648
}
623649

@@ -810,29 +836,69 @@ func runClientCmd(cmd *cobra.Command, args []string) {
810836
runClient(defaultViper)
811837
}
812838

839+
// authRetryDelay is how long to wait before asking a second time about a refused
840+
// password. Short enough to stay inside the restart floor that is being spent
841+
// anyway, long enough to outlast the momentary gap it exists to tolerate.
842+
const authRetryDelay = 2 * time.Second
843+
844+
// connectClient builds the client, and when the server refuses the password asks
845+
// once more before believing it.
846+
//
847+
// AuthError carries no more meaning than "the server answered with something
848+
// other than 233". A server whose auth backend is momentarily unavailable serves
849+
// the request through its masquerade handler instead -- a 404 -- which is the
850+
// same answer a genuinely wrong password gets, and the client cannot tell them
851+
// apart. With lazy off this handshake is a single un-retried attempt, and the
852+
// caller turns an AuthError into a status file that makes the netifd handler
853+
// call proto_block_restart. One unlucky moment would then hold the WAN down
854+
// until somebody logged in and ran ifup, where before it recovered by itself.
855+
//
856+
// Asking twice separates the two: a wrong password is refused both times, a gap
857+
// almost never lasts across the delay. Only an auth failure is retried -- every
858+
// other error is already treated as retryable and is left to netifd.
859+
func connectClient(config clientConfig, connected func(client.Client, *client.HandshakeInfo, int)) (client.Client, error) {
860+
c, err := client.NewReconnectableClient(config.Config, connected, config.Lazy)
861+
var authErr coreErrs.AuthError
862+
if err == nil || !errors.As(err, &authErr) {
863+
return c, err
864+
}
865+
logger.Warn("server refused the password, confirming before reporting it as final",
866+
zap.Int("status", authErr.StatusCode))
867+
time.Sleep(authRetryDelay)
868+
return client.NewReconnectableClient(config.Config, connected, config.Lazy)
869+
}
870+
813871
func runClient(v *viper.Viper) {
814872
if err := v.ReadInConfig(); err != nil {
873+
holdPPPRestart()
815874
logger.Fatal("failed to read client config", zap.Error(err))
816875
}
817876
var config clientConfig
818877
if err := v.Unmarshal(&config); err != nil {
878+
holdPPPRestart()
819879
logger.Fatal("failed to parse client config", zap.Error(err))
820880
}
821881

822-
c, err := client.NewReconnectableClient(
823-
config.Config,
824-
func(c client.Client, info *client.HandshakeInfo, count int) {
825-
connectLog(info, count)
826-
// On the client side, we start checking for updates after we successfully connect
827-
// to the server, which, depending on whether lazy mode is enabled, may or may not
828-
// be immediately after the client starts. We don't want the update check request
829-
// to interfere with the lazy mode option.
830-
if count == 1 && !disableUpdateCheck {
831-
go runCheckUpdateClient(c)
832-
}
833-
}, config.Lazy,
834-
)
882+
connected := func(c client.Client, info *client.HandshakeInfo, count int) {
883+
connectLog(info, count)
884+
// On the client side, we start checking for updates after we successfully connect
885+
// to the server, which, depending on whether lazy mode is enabled, may or may not
886+
// be immediately after the client starts. We don't want the update check request
887+
// to interfere with the lazy mode option.
888+
if count == 1 && !disableUpdateCheck {
889+
go runCheckUpdateClient(c)
890+
}
891+
}
892+
893+
c, err := connectClient(config, connected)
835894
if err != nil {
895+
// Two unrelated things share this line. The status write is where a
896+
// refused Hysteria2 password is reported from: with lazy off the
897+
// handshake has already happened here, so it never reaches the runner
898+
// below. The hold is for the opposite case -- a dropped upstream fails
899+
// this dial in microseconds, and without it that becomes a restart loop.
900+
writePPPStatus(err)
901+
holdPPPRestart()
836902
logger.Fatal("failed to initialize client", zap.Error(err))
837903
}
838904
defer c.Close()
@@ -887,6 +953,11 @@ func runClient(v *viper.Viper) {
887953
return clientTUN(*config.TUN, c)
888954
})
889955
}
956+
if config.PPP != nil {
957+
runner.Add("PPP", func() error {
958+
return clientPPP(*config.PPP, c, strings.EqualFold(config.Obfs.Type, "salamander"))
959+
})
960+
}
890961

891962
signalChan := make(chan os.Signal, 1)
892963
signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM)
@@ -904,7 +975,30 @@ func runClient(v *viper.Viper) {
904975
if r.OK {
905976
logger.Info(r.Msg)
906977
} else {
978+
// A connect that fails only reaches here in lazy mode, where the
979+
// handshake is deferred until PPP mode asks for the conn. With lazy
980+
// off -- which is what the OpenWrt handler generates, since it never
981+
// writes the key -- the handshake has already happened above, and a
982+
// refused password fails there instead. Both sites write the status.
983+
writePPPStatus(r.Err)
907984
_ = c.Close() // Close the client here as Fatal will exit the program without running defer
985+
// Hand SIGTERM back to the runtime before holding. The deferred Stop
986+
// never runs -- Fatal exits -- so without this an ifdown arriving
987+
// during the hold would sit unread in signalChan and be ignored for
988+
// the length of it, keeping netifd waiting on a process that has
989+
// already decided to die.
990+
signal.Stop(signalChan)
991+
// Stop only restores the default disposition for signals still to
992+
// come. One that arrived while this select was being decided is
993+
// already buffered, and the select is free to have picked the runner
994+
// case instead -- so it has to be looked for, not waited for. Finding
995+
// one means an ifdown is in progress and nothing is going to restart
996+
// us, which is exactly when holding is pure delay.
997+
select {
998+
case <-signalChan:
999+
default:
1000+
holdPPPRestart()
1001+
}
9081002
if r.Err != nil {
9091003
logger.Fatal(r.Msg, zap.Error(r.Err))
9101004
} else {

app/cmd/client_ppp.go

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
package cmd
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"net"
7+
"strconv"
8+
"strings"
9+
10+
"go.uber.org/zap"
11+
12+
"github.com/apernet/hysteria/app/v2/internal/ppp"
13+
"github.com/apernet/hysteria/core/v2/client"
14+
"github.com/apernet/hysteria/extras/v2/pppbridge"
15+
)
16+
17+
// pppdInvocation is everything clientPPP decides before anything is spawned:
18+
// which program to run, with what arguments, and whether to route the server's
19+
// address around the tunnel.
20+
//
21+
// Separated from clientPPP because the decision is where all the branching is --
22+
// nospawn, SSTP, an explicit MTU or a measured one -- while what follows it is a
23+
// Serve loop that blocks until the process ends. Kept together they could only
24+
// be checked by running a real pppd.
25+
type pppdInvocation struct {
26+
path string
27+
args []string
28+
serverRoute bool
29+
noSpawn bool
30+
}
31+
32+
func clientPPP(config pppConfig, c client.Client, salamander bool) error {
33+
inv, err := clientPPPDInvocation(config, c.RemoteAddr(), salamander)
34+
if err != nil {
35+
return err
36+
}
37+
38+
if inv.noSpawn {
39+
logger.Info("PPP mode starting",
40+
zap.String("mode", "nospawn"),
41+
zap.Int("dataStreams", config.DataStreams))
42+
s := &ppp.Server{
43+
HyClient: c,
44+
Logger: logger,
45+
DataStreams: config.DataStreams,
46+
NoSpawn: true,
47+
// Whatever started pppd chose its MRU; ppp.mtu is how it tells us. Left
48+
// unset the transport is measured instead, which still works -- pppd
49+
// simply never learns the answer -- but costs a dial's latency for
50+
// nothing. The netifd handler always sets it.
51+
LinkMRU: int(config.MTU),
52+
}
53+
err := s.Serve()
54+
writePPPStatus(err)
55+
return err
56+
}
57+
58+
logger.Info("PPP mode starting",
59+
zap.String("pppdPath", inv.path),
60+
zap.Strings("pppdArgs", inv.args),
61+
zap.Int("dataStreams", config.DataStreams),
62+
zap.Bool("serverRoute", inv.serverRoute))
63+
64+
s := &ppp.Server{
65+
HyClient: c,
66+
Logger: logger,
67+
PPPDPath: inv.path,
68+
PPPDArgs: inv.args,
69+
DataStreams: config.DataStreams,
70+
ServerRoute: inv.serverRoute,
71+
}
72+
73+
return s.Serve()
74+
}
75+
76+
func clientPPPDInvocation(config pppConfig, remoteAddr net.Addr, salamander bool) (pppdInvocation, error) {
77+
noSpawn := false
78+
switch strings.ToLower(config.Mode) {
79+
case "", "local":
80+
case "nospawn":
81+
noSpawn = true
82+
default:
83+
return pppdInvocation{}, configError{Field: "ppp.mode", Err: fmt.Errorf("unsupported mode %q (must be \"local\" or \"nospawn\")", config.Mode)}
84+
}
85+
86+
if noSpawn {
87+
if config.SSTP != nil {
88+
return pppdInvocation{}, configError{Field: "ppp.mode", Err: errors.New("nospawn mode cannot be combined with ppp.sstp")}
89+
}
90+
return pppdInvocation{noSpawn: true}, nil
91+
}
92+
93+
pppdPath := config.PPPDPath
94+
pppdArgs := config.PPPDArgs
95+
96+
if len(pppdArgs) == 0 {
97+
// LCP echo is the only thing that notices a link which is nominally up but
98+
// silently dropping frames -- an MTU black hole, a stale NAT binding, a
99+
// middlebox eating large UDP. It travels the same transport as user data,
100+
// so it genuinely probes the path the data takes. Adaptive keeps it quiet
101+
// while traffic is flowing; 3 misses at 5s gives ~15s to detection, well
102+
// inside the 30s QUIC idle timeout.
103+
pppdArgs = []string{
104+
"nodetach", "local", "+ipv6", "multilink",
105+
"lcp-echo-interval", "5", "lcp-echo-failure", "3", "lcp-echo-adaptive",
106+
}
107+
if config.MTU > 0 {
108+
s := strconv.Itoa(int(config.MTU))
109+
pppdArgs = append(pppdArgs, "mtu", s, "mru", s)
110+
} else {
111+
linkMRU := pppbridge.AutoPPPMTU(pppbridge.MTUParams{
112+
RemoteAddr: remoteAddr,
113+
Salamander: salamander,
114+
DataStreams: config.DataStreams,
115+
})
116+
vpnMTU := linkMRU - pppbridge.MLPPPOverhead
117+
if config.SSTP != nil {
118+
s := strconv.Itoa(linkMRU)
119+
pppdArgs = append(pppdArgs, "mtu", s, "mru", s)
120+
} else {
121+
pppdArgs = append(pppdArgs, "mtu", strconv.Itoa(vpnMTU), "mru", strconv.Itoa(linkMRU))
122+
}
123+
}
124+
}
125+
126+
serverRoute := false
127+
if config.SSTP != nil {
128+
if config.SSTP.LogLevel == "" {
129+
config.SSTP.LogLevel = logLevel
130+
}
131+
132+
if pppdPath == "" {
133+
if config.SSTP.BinaryPath != "" {
134+
pppdPath = config.SSTP.BinaryPath
135+
} else {
136+
pppdPath = "ppp-sstp"
137+
}
138+
}
139+
140+
sstpArgs := buildSSTPArgs(config.SSTP)
141+
pppdArgs = append(sstpArgs, pppdArgs...)
142+
143+
serverRoute = true
144+
if config.SSTP.ServerRoute != nil {
145+
serverRoute = *config.SSTP.ServerRoute
146+
}
147+
} else if pppdPath == "" {
148+
pppdPath = "pppd"
149+
}
150+
151+
return pppdInvocation{path: pppdPath, args: pppdArgs, serverRoute: serverRoute}, nil
152+
}
153+
154+
// buildSSTPArgs generates command-line arguments for the ppp-sstp binary.
155+
func buildSSTPArgs(cfg *pppSSTPConfig) []string {
156+
var args []string
157+
if cfg.LogLevel != "" {
158+
args = append(args, "-l", cfg.LogLevel)
159+
}
160+
161+
listen := cfg.Listen
162+
if listen == "" {
163+
listen = "127.0.0.1:8443"
164+
}
165+
args = append(args, "listen", listen)
166+
167+
if cfg.CertDir != "" {
168+
args = append(args, "cert-dir", cfg.CertDir)
169+
}
170+
if cfg.Endpoint != "" {
171+
args = append(args, "endpoint", cfg.Endpoint)
172+
}
173+
if cfg.User != "" {
174+
args = append(args, "user", cfg.User)
175+
}
176+
if cfg.Password != "" {
177+
args = append(args, "password", cfg.Password)
178+
}
179+
if cfg.MSSClamp != nil {
180+
args = append(args, "mss-clamp", strconv.Itoa(*cfg.MSSClamp))
181+
}
182+
return args
183+
}

0 commit comments

Comments
 (0)