Skip to content

Commit 3060b8d

Browse files
committed
Add PPP over Hysteria2
1 parent 3c9dd62 commit 3060b8d

Some content is hidden

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

52 files changed

+6156
-38
lines changed

app/cmd/client.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,27 @@ type clientConfig struct {
7676
UDPTProxy *udpTProxyConfig `mapstructure:"udpTProxy"`
7777
TCPRedirect *tcpRedirectConfig `mapstructure:"tcpRedirect"`
7878
TUN *tunConfig `mapstructure:"tun"`
79+
PPP *pppConfig `mapstructure:"ppp"`
80+
}
81+
82+
type pppSSTPConfig struct {
83+
BinaryPath string `mapstructure:"binaryPath"`
84+
Listen string `mapstructure:"listen"`
85+
CertDir string `mapstructure:"certDir"`
86+
Endpoint string `mapstructure:"endpoint"`
87+
User string `mapstructure:"user"`
88+
Password string `mapstructure:"password"`
89+
MSSClamp *int `mapstructure:"mssClamp"` // nil=auto, 0=off, >0=forced
90+
ServerRoute *bool `mapstructure:"serverRoute"`
91+
LogLevel string `mapstructure:"logLevel"`
92+
}
93+
94+
type pppConfig struct {
95+
MTU uint32 `mapstructure:"mtu"`
96+
PPPDPath string `mapstructure:"pppdPath"`
97+
PPPDArgs []string `mapstructure:"pppdArgs"`
98+
DataStreams int `mapstructure:"dataStreams"`
99+
SSTP *pppSSTPConfig `mapstructure:"sstp"`
79100
}
80101

81102
type clientConfigTransportUDP struct {
@@ -465,6 +486,9 @@ func (c *clientConfig) Config() (*client.Config, error) {
465486
return nil, err
466487
}
467488
}
489+
if c.PPP != nil && c.PPP.DataStreams == 0 {
490+
hyConfig.PPPMode = true
491+
}
468492
return hyConfig, nil
469493
}
470494

@@ -549,6 +573,11 @@ func runClient(v *viper.Viper) {
549573
return clientTUN(*config.TUN, c)
550574
})
551575
}
576+
if config.PPP != nil {
577+
runner.Add("PPP", func() error {
578+
return clientPPP(*config.PPP, c, strings.EqualFold(config.Obfs.Type, "salamander"))
579+
})
580+
}
552581

553582
signalChan := make(chan os.Signal, 1)
554583
signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM)

app/cmd/client_ppp.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package cmd
2+
3+
import (
4+
"strconv"
5+
6+
"go.uber.org/zap"
7+
8+
"github.com/apernet/hysteria/app/v2/internal/ppp"
9+
"github.com/apernet/hysteria/core/v2/client"
10+
"github.com/apernet/hysteria/extras/v2/pppbridge"
11+
)
12+
13+
func clientPPP(config pppConfig, c client.Client, salamander bool) error {
14+
pppdPath := config.PPPDPath
15+
pppdArgs := config.PPPDArgs
16+
17+
if len(pppdArgs) == 0 {
18+
pppdArgs = []string{"nodetach", "local", "+ipv6", "multilink", "lcp-echo-interval", "0"}
19+
if config.MTU > 0 {
20+
s := strconv.Itoa(int(config.MTU))
21+
pppdArgs = append(pppdArgs, "mtu", s, "mru", s)
22+
} else {
23+
linkMRU := pppbridge.AutoPPPMTU(pppbridge.MTUParams{
24+
RemoteAddr: c.RemoteAddr(),
25+
Salamander: salamander,
26+
DataStreams: config.DataStreams,
27+
Multilink: false,
28+
})
29+
vpnMTU := linkMRU - pppbridge.MLPPPOverhead
30+
if config.SSTP != nil {
31+
s := strconv.Itoa(linkMRU)
32+
pppdArgs = append(pppdArgs, "mtu", s, "mru", s)
33+
} else {
34+
pppdArgs = append(pppdArgs, "mtu", strconv.Itoa(vpnMTU), "mru", strconv.Itoa(linkMRU))
35+
}
36+
}
37+
}
38+
39+
serverRoute := false
40+
if config.SSTP != nil {
41+
if config.SSTP.LogLevel == "" {
42+
config.SSTP.LogLevel = logLevel
43+
}
44+
45+
if pppdPath == "" {
46+
if config.SSTP.BinaryPath != "" {
47+
pppdPath = config.SSTP.BinaryPath
48+
} else {
49+
pppdPath = "ppp-sstp"
50+
}
51+
}
52+
53+
sstpArgs := buildSSTPArgs(config.SSTP)
54+
pppdArgs = append(sstpArgs, pppdArgs...)
55+
56+
serverRoute = true
57+
if config.SSTP.ServerRoute != nil {
58+
serverRoute = *config.SSTP.ServerRoute
59+
}
60+
} else if pppdPath == "" {
61+
pppdPath = "pppd"
62+
}
63+
64+
logger.Info("PPP mode starting",
65+
zap.String("pppdPath", pppdPath),
66+
zap.Strings("pppdArgs", pppdArgs),
67+
zap.Int("dataStreams", config.DataStreams),
68+
zap.Bool("serverRoute", serverRoute))
69+
70+
s := &ppp.Server{
71+
HyClient: c,
72+
Logger: logger,
73+
PPPDPath: pppdPath,
74+
PPPDArgs: pppdArgs,
75+
DataStreams: config.DataStreams,
76+
ServerRoute: serverRoute,
77+
}
78+
79+
return s.Serve()
80+
}
81+
82+
// buildSSTPArgs generates command-line arguments for the ppp-sstp binary.
83+
func buildSSTPArgs(cfg *pppSSTPConfig) []string {
84+
var args []string
85+
if cfg.LogLevel != "" {
86+
args = append(args, "-l", cfg.LogLevel)
87+
}
88+
89+
listen := cfg.Listen
90+
if listen == "" {
91+
listen = "127.0.0.1:8443"
92+
}
93+
args = append(args, "listen", listen)
94+
95+
if cfg.CertDir != "" {
96+
args = append(args, "cert-dir", cfg.CertDir)
97+
}
98+
if cfg.Endpoint != "" {
99+
args = append(args, "endpoint", cfg.Endpoint)
100+
}
101+
if cfg.User != "" {
102+
args = append(args, "user", cfg.User)
103+
}
104+
if cfg.Password != "" {
105+
args = append(args, "password", cfg.Password)
106+
}
107+
if cfg.MSSClamp != nil {
108+
args = append(args, "mss-clamp", strconv.Itoa(*cfg.MSSClamp))
109+
}
110+
return args
111+
}

app/cmd/client_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,12 @@ func TestClientConfig(t *testing.T) {
9595
TCPRedirect: &tcpRedirectConfig{
9696
Listen: "127.0.0.1:3500",
9797
},
98+
PPP: &pppConfig{
99+
MTU: 1400,
100+
PPPDPath: "/usr/sbin/pppd",
101+
PPPDArgs: []string{"defaultroute", "+ipv6"},
102+
DataStreams: 20,
103+
},
98104
TUN: &tunConfig{
99105
Name: "hytun",
100106
MTU: 1500,

app/cmd/client_test.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,14 @@ udpTProxy:
7272
tcpRedirect:
7373
listen: 127.0.0.1:3500
7474

75+
ppp:
76+
mtu: 1400
77+
pppdPath: "/usr/sbin/pppd"
78+
pppdArgs:
79+
- defaultroute
80+
- "+ipv6"
81+
dataStreams: 20 # 0 or omit for datagram mode, >0 for multi-stream mode
82+
7583
tun:
7684
name: "hytun"
7785
mtu: 1500

app/cmd/server.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,41 @@ type serverConfig struct {
7272
Outbounds []serverConfigOutboundEntry `mapstructure:"outbounds"`
7373
TrafficStats serverConfigTrafficStats `mapstructure:"trafficStats"`
7474
Masquerade serverConfigMasquerade `mapstructure:"masquerade"`
75+
PPP pppServerConfig `mapstructure:"ppp"`
76+
}
77+
78+
type pppServerConfig struct {
79+
Enabled bool `mapstructure:"enabled"`
80+
Mode string `mapstructure:"mode"` // "local" (default) or "l2tp"
81+
PPPDPath string `mapstructure:"pppdPath"`
82+
PPPDArgs []string `mapstructure:"pppdArgs"`
83+
Sudo bool `mapstructure:"sudo"`
84+
IPv4Pool string `mapstructure:"ipv4Pool"`
85+
DNS []string `mapstructure:"dns"`
86+
MTU uint32 `mapstructure:"mtu"`
87+
L2TP pppL2TPConfig `mapstructure:"l2tp"`
88+
}
89+
90+
type pppL2TPConfig struct {
91+
Hostname string `mapstructure:"hostname"`
92+
HelloInterval int `mapstructure:"helloInterval"`
93+
Groups map[string]pppL2TPGroupConfig `mapstructure:"groups"`
94+
Realms []pppRealmConfig `mapstructure:"realms"`
95+
}
96+
97+
type pppL2TPGroupConfig struct {
98+
LNS []pppLNSConfig `mapstructure:"lns"`
99+
}
100+
101+
type pppLNSConfig struct {
102+
Address string `mapstructure:"address"`
103+
Secret string `mapstructure:"secret"`
104+
Weight int `mapstructure:"weight"`
105+
}
106+
107+
type pppRealmConfig struct {
108+
Pattern string `mapstructure:"pattern"`
109+
Group string `mapstructure:"group"`
75110
}
76111

77112
type serverConfigObfsSalamander struct {
@@ -926,6 +961,7 @@ func (c *serverConfig) Config() (*server.Config, error) {
926961
c.fillEventLogger,
927962
c.fillTrafficLogger,
928963
c.fillMasqHandler,
964+
c.fillPPPConfig,
929965
}
930966
for _, f := range fillers {
931967
if err := f(hyConfig); err != nil {

app/cmd/server_ppp.go

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"strings"
7+
"time"
8+
9+
"github.com/apernet/hysteria/core/v2/server"
10+
"github.com/apernet/hysteria/extras/v2/l2tp"
11+
"github.com/apernet/hysteria/extras/v2/pppbridge"
12+
"go.uber.org/zap"
13+
)
14+
15+
func (c *serverConfig) fillPPPConfig(hyConfig *server.Config) error {
16+
if !c.PPP.Enabled {
17+
return nil
18+
}
19+
20+
mode := strings.ToLower(c.PPP.Mode)
21+
if mode == "" {
22+
mode = "local"
23+
}
24+
25+
switch mode {
26+
case "local":
27+
return c.fillPPPConfigLocal(hyConfig)
28+
case "l2tp":
29+
return c.fillPPPConfigL2TP(hyConfig)
30+
default:
31+
return configError{Field: "ppp.mode", Err: fmt.Errorf("unsupported mode %q (must be \"local\" or \"l2tp\")", c.PPP.Mode)}
32+
}
33+
}
34+
35+
func (c *serverConfig) fillPPPConfigLocal(hyConfig *server.Config) error {
36+
pppdPath := c.PPP.PPPDPath
37+
if pppdPath == "" {
38+
pppdPath = "/usr/sbin/pppd"
39+
}
40+
41+
var pool *pppbridge.IPPool
42+
if c.PPP.IPv4Pool != "" {
43+
var err error
44+
pool, err = pppbridge.NewIPPool(c.PPP.IPv4Pool)
45+
if err != nil {
46+
return configError{Field: "ppp.ipv4Pool", Err: err}
47+
}
48+
}
49+
50+
poolDesc := "(IPv6-only)"
51+
if c.PPP.IPv4Pool != "" {
52+
poolDesc = c.PPP.IPv4Pool
53+
}
54+
logger.Info("PPP enabled (local mode)",
55+
zap.String("ipv4Pool", poolDesc),
56+
zap.String("pppdPath", pppdPath),
57+
zap.Bool("sudo", c.PPP.Sudo),
58+
zap.Uint32("mtu", c.PPP.MTU))
59+
60+
hyConfig.PPPRequestHandler = &pppbridge.ServerPPPHandler{
61+
PPPDPath: pppdPath,
62+
PPPDArgs: c.PPP.PPPDArgs,
63+
Sudo: c.PPP.Sudo,
64+
IPv4Pool: pool,
65+
DNS: c.PPP.DNS,
66+
MTU: int(c.PPP.MTU),
67+
Salamander: strings.EqualFold(c.Obfs.Type, "salamander"),
68+
Logger: logger,
69+
}
70+
return nil
71+
}
72+
73+
func (c *serverConfig) fillPPPConfigL2TP(hyConfig *server.Config) error {
74+
l2tpCfg := c.PPP.L2TP
75+
76+
if l2tpCfg.Hostname == "" {
77+
l2tpCfg.Hostname = os.Getenv("HYSTERIA_LAC_HOSTNAME")
78+
}
79+
if l2tpCfg.Hostname == "" {
80+
l2tpCfg.Hostname, _ = os.Hostname()
81+
}
82+
if l2tpCfg.Hostname == "" {
83+
return configError{Field: "ppp.l2tp.hostname", Err: fmt.Errorf("hostname is required but could not be determined")}
84+
}
85+
86+
// Validate groups
87+
if len(l2tpCfg.Groups) == 0 {
88+
return configError{Field: "ppp.l2tp.groups", Err: fmt.Errorf("at least one LNS group is required")}
89+
}
90+
lbGroups := make(map[string][]l2tp.LNSConfig)
91+
for name, group := range l2tpCfg.Groups {
92+
if len(group.LNS) == 0 {
93+
return configError{Field: fmt.Sprintf("ppp.l2tp.groups.%s.lns", name), Err: fmt.Errorf("at least one LNS is required")}
94+
}
95+
for _, lns := range group.LNS {
96+
if lns.Address == "" {
97+
return configError{Field: fmt.Sprintf("ppp.l2tp.groups.%s.lns.address", name), Err: fmt.Errorf("LNS address is required")}
98+
}
99+
w := lns.Weight
100+
if w <= 0 {
101+
w = 1
102+
}
103+
lbGroups[name] = append(lbGroups[name], l2tp.LNSConfig{
104+
Address: lns.Address,
105+
Secret: lns.Secret,
106+
Weight: w,
107+
})
108+
}
109+
}
110+
111+
// Validate realms
112+
if len(l2tpCfg.Realms) == 0 {
113+
return configError{Field: "ppp.l2tp.realms", Err: fmt.Errorf("at least one realm rule is required")}
114+
}
115+
var realmRules []l2tp.RealmRule
116+
for _, realm := range l2tpCfg.Realms {
117+
if realm.Pattern == "" {
118+
return configError{Field: "ppp.l2tp.realms.pattern", Err: fmt.Errorf("realm pattern is required")}
119+
}
120+
if _, ok := l2tpCfg.Groups[realm.Group]; !ok {
121+
return configError{Field: "ppp.l2tp.realms.group", Err: fmt.Errorf("realm references unknown group %q", realm.Group)}
122+
}
123+
realmRules = append(realmRules, l2tp.RealmRule{
124+
Pattern: realm.Pattern,
125+
Group: realm.Group,
126+
})
127+
}
128+
129+
helloInterval := time.Duration(l2tpCfg.HelloInterval) * time.Second
130+
131+
tm := l2tp.NewTunnelManager(l2tpCfg.Hostname, helloInterval, logger)
132+
rr := l2tp.NewRealmRouter(realmRules)
133+
lb := l2tp.NewLoadBalancer(lbGroups)
134+
135+
logger.Info("PPP enabled (L2TP mode)",
136+
zap.String("hostname", l2tpCfg.Hostname),
137+
zap.Int("helloInterval", l2tpCfg.HelloInterval),
138+
zap.Int("groups", len(l2tpCfg.Groups)),
139+
zap.Int("realms", len(l2tpCfg.Realms)))
140+
141+
hyConfig.PPPRequestHandler = &pppbridge.L2TPPPPHandler{
142+
TunnelManager: tm,
143+
RealmRouter: rr,
144+
LoadBalancer: lb,
145+
Logger: logger,
146+
}
147+
return nil
148+
}

0 commit comments

Comments
 (0)