Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions app/cmd/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,27 @@ type clientConfig struct {
UDPTProxy *udpTProxyConfig `mapstructure:"udpTProxy"`
TCPRedirect *tcpRedirectConfig `mapstructure:"tcpRedirect"`
TUN *tunConfig `mapstructure:"tun"`
PPP *pppConfig `mapstructure:"ppp"`
}

type pppSSTPConfig struct {
BinaryPath string `mapstructure:"binaryPath"`
Listen string `mapstructure:"listen"`
CertDir string `mapstructure:"certDir"`
Endpoint string `mapstructure:"endpoint"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
MSSClamp *int `mapstructure:"mssClamp"` // nil=auto, 0=off, >0=forced
ServerRoute *bool `mapstructure:"serverRoute"`
LogLevel string `mapstructure:"logLevel"`
}

type pppConfig struct {
MTU uint32 `mapstructure:"mtu"`
PPPDPath string `mapstructure:"pppdPath"`
PPPDArgs []string `mapstructure:"pppdArgs"`
DataStreams int `mapstructure:"dataStreams"`
SSTP *pppSSTPConfig `mapstructure:"sstp"`
}

type clientConfigTransportUDP struct {
Expand Down Expand Up @@ -512,6 +533,9 @@ func (c *clientConfig) Config() (*client.Config, error) {
return nil, err
}
}
if c.PPP != nil && c.PPP.DataStreams == 0 {
hyConfig.PPPMode = true
}
return hyConfig, nil
}

Expand Down Expand Up @@ -596,6 +620,11 @@ func runClient(v *viper.Viper) {
return clientTUN(*config.TUN, c)
})
}
if config.PPP != nil {
runner.Add("PPP", func() error {
return clientPPP(*config.PPP, c, strings.EqualFold(config.Obfs.Type, "salamander"))
})
}

signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM)
Expand Down
111 changes: 111 additions & 0 deletions app/cmd/client_ppp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package cmd

import (
"strconv"

"go.uber.org/zap"

"github.com/apernet/hysteria/app/v2/internal/ppp"
"github.com/apernet/hysteria/core/v2/client"
"github.com/apernet/hysteria/extras/v2/pppbridge"
)

func clientPPP(config pppConfig, c client.Client, salamander bool) error {
pppdPath := config.PPPDPath
pppdArgs := config.PPPDArgs

if len(pppdArgs) == 0 {
pppdArgs = []string{"nodetach", "local", "+ipv6", "multilink", "lcp-echo-interval", "0"}
if config.MTU > 0 {
s := strconv.Itoa(int(config.MTU))
pppdArgs = append(pppdArgs, "mtu", s, "mru", s)
} else {
linkMRU := pppbridge.AutoPPPMTU(pppbridge.MTUParams{
RemoteAddr: c.RemoteAddr(),
Salamander: salamander,
DataStreams: config.DataStreams,
Multilink: false,
})
vpnMTU := linkMRU - pppbridge.MLPPPOverhead
if config.SSTP != nil {
s := strconv.Itoa(linkMRU)
pppdArgs = append(pppdArgs, "mtu", s, "mru", s)
} else {
pppdArgs = append(pppdArgs, "mtu", strconv.Itoa(vpnMTU), "mru", strconv.Itoa(linkMRU))
}
}
}

serverRoute := false
if config.SSTP != nil {
if config.SSTP.LogLevel == "" {
config.SSTP.LogLevel = logLevel
}

if pppdPath == "" {
if config.SSTP.BinaryPath != "" {
pppdPath = config.SSTP.BinaryPath
} else {
pppdPath = "ppp-sstp"
}
}

sstpArgs := buildSSTPArgs(config.SSTP)
pppdArgs = append(sstpArgs, pppdArgs...)

serverRoute = true
if config.SSTP.ServerRoute != nil {
serverRoute = *config.SSTP.ServerRoute
}
} else if pppdPath == "" {
pppdPath = "pppd"
}

logger.Info("PPP mode starting",
zap.String("pppdPath", pppdPath),
zap.Strings("pppdArgs", pppdArgs),
zap.Int("dataStreams", config.DataStreams),
zap.Bool("serverRoute", serverRoute))

s := &ppp.Server{
HyClient: c,
Logger: logger,
PPPDPath: pppdPath,
PPPDArgs: pppdArgs,
DataStreams: config.DataStreams,
ServerRoute: serverRoute,
}

return s.Serve()
}

// buildSSTPArgs generates command-line arguments for the ppp-sstp binary.
func buildSSTPArgs(cfg *pppSSTPConfig) []string {
var args []string
if cfg.LogLevel != "" {
args = append(args, "-l", cfg.LogLevel)
}

listen := cfg.Listen
if listen == "" {
listen = "127.0.0.1:8443"
}
args = append(args, "listen", listen)

if cfg.CertDir != "" {
args = append(args, "cert-dir", cfg.CertDir)
}
if cfg.Endpoint != "" {
args = append(args, "endpoint", cfg.Endpoint)
}
if cfg.User != "" {
args = append(args, "user", cfg.User)
}
if cfg.Password != "" {
args = append(args, "password", cfg.Password)
}
if cfg.MSSClamp != nil {
args = append(args, "mss-clamp", strconv.Itoa(*cfg.MSSClamp))
}
return args
}
6 changes: 6 additions & 0 deletions app/cmd/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ func TestClientConfig(t *testing.T) {
TCPRedirect: &tcpRedirectConfig{
Listen: "127.0.0.1:3500",
},
PPP: &pppConfig{
MTU: 1400,
PPPDPath: "/usr/sbin/pppd",
PPPDArgs: []string{"defaultroute", "+ipv6"},
DataStreams: 20,
},
TUN: &tunConfig{
Name: "hytun",
MTU: 1500,
Expand Down
8 changes: 8 additions & 0 deletions app/cmd/client_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,14 @@ udpTProxy:
tcpRedirect:
listen: 127.0.0.1:3500

ppp:
mtu: 1400
pppdPath: "/usr/sbin/pppd"
pppdArgs:
- defaultroute
- "+ipv6"
dataStreams: 20 # 0 or omit for datagram mode, >0 for multi-stream mode

tun:
name: "hytun"
mtu: 1500
Expand Down
36 changes: 36 additions & 0 deletions app/cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,41 @@ type serverConfig struct {
Outbounds []serverConfigOutboundEntry `mapstructure:"outbounds"`
TrafficStats serverConfigTrafficStats `mapstructure:"trafficStats"`
Masquerade serverConfigMasquerade `mapstructure:"masquerade"`
PPP pppServerConfig `mapstructure:"ppp"`
}

type pppServerConfig struct {
Enabled bool `mapstructure:"enabled"`
Mode string `mapstructure:"mode"` // "local" (default) or "l2tp"
PPPDPath string `mapstructure:"pppdPath"`
PPPDArgs []string `mapstructure:"pppdArgs"`
Sudo bool `mapstructure:"sudo"`
IPv4Pool string `mapstructure:"ipv4Pool"`
DNS []string `mapstructure:"dns"`
MTU uint32 `mapstructure:"mtu"`
L2TP pppL2TPConfig `mapstructure:"l2tp"`
}

type pppL2TPConfig struct {
Hostname string `mapstructure:"hostname"`
HelloInterval int `mapstructure:"helloInterval"`
Groups map[string]pppL2TPGroupConfig `mapstructure:"groups"`
Realms []pppRealmConfig `mapstructure:"realms"`
}

type pppL2TPGroupConfig struct {
LNS []pppLNSConfig `mapstructure:"lns"`
}

type pppLNSConfig struct {
Address string `mapstructure:"address"`
Secret string `mapstructure:"secret"`
Weight int `mapstructure:"weight"`
}

type pppRealmConfig struct {
Pattern string `mapstructure:"pattern"`
Group string `mapstructure:"group"`
}

type serverConfigObfsSalamander struct {
Expand Down Expand Up @@ -991,6 +1026,7 @@ func (c *serverConfig) Config() (*server.Config, error) {
c.fillEventLogger,
c.fillTrafficLogger,
c.fillMasqHandler,
c.fillPPPConfig,
}
for _, f := range fillers {
if err := f(hyConfig); err != nil {
Expand Down
148 changes: 148 additions & 0 deletions app/cmd/server_ppp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package cmd

import (
"fmt"
"os"
"strings"
"time"

"github.com/apernet/hysteria/core/v2/server"
"github.com/apernet/hysteria/extras/v2/l2tp"
"github.com/apernet/hysteria/extras/v2/pppbridge"
"go.uber.org/zap"
)

func (c *serverConfig) fillPPPConfig(hyConfig *server.Config) error {
if !c.PPP.Enabled {
return nil
}

mode := strings.ToLower(c.PPP.Mode)
if mode == "" {
mode = "local"
}

switch mode {
case "local":
return c.fillPPPConfigLocal(hyConfig)
case "l2tp":
return c.fillPPPConfigL2TP(hyConfig)
default:
return configError{Field: "ppp.mode", Err: fmt.Errorf("unsupported mode %q (must be \"local\" or \"l2tp\")", c.PPP.Mode)}
}
}

func (c *serverConfig) fillPPPConfigLocal(hyConfig *server.Config) error {
pppdPath := c.PPP.PPPDPath
if pppdPath == "" {
pppdPath = "/usr/sbin/pppd"
}

var pool *pppbridge.IPPool
if c.PPP.IPv4Pool != "" {
var err error
pool, err = pppbridge.NewIPPool(c.PPP.IPv4Pool)
if err != nil {
return configError{Field: "ppp.ipv4Pool", Err: err}
}
}

poolDesc := "(IPv6-only)"
if c.PPP.IPv4Pool != "" {
poolDesc = c.PPP.IPv4Pool
}
logger.Info("PPP enabled (local mode)",
zap.String("ipv4Pool", poolDesc),
zap.String("pppdPath", pppdPath),
zap.Bool("sudo", c.PPP.Sudo),
zap.Uint32("mtu", c.PPP.MTU))

hyConfig.PPPRequestHandler = &pppbridge.ServerPPPHandler{
PPPDPath: pppdPath,
PPPDArgs: c.PPP.PPPDArgs,
Sudo: c.PPP.Sudo,
IPv4Pool: pool,
DNS: c.PPP.DNS,
MTU: int(c.PPP.MTU),
Salamander: strings.EqualFold(c.Obfs.Type, "salamander"),
Logger: logger,
}
return nil
}

func (c *serverConfig) fillPPPConfigL2TP(hyConfig *server.Config) error {
l2tpCfg := c.PPP.L2TP

if l2tpCfg.Hostname == "" {
l2tpCfg.Hostname = os.Getenv("HYSTERIA_LAC_HOSTNAME")
}
if l2tpCfg.Hostname == "" {
l2tpCfg.Hostname, _ = os.Hostname()
}
if l2tpCfg.Hostname == "" {
return configError{Field: "ppp.l2tp.hostname", Err: fmt.Errorf("hostname is required but could not be determined")}
}

// Validate groups
if len(l2tpCfg.Groups) == 0 {
return configError{Field: "ppp.l2tp.groups", Err: fmt.Errorf("at least one LNS group is required")}
}
lbGroups := make(map[string][]l2tp.LNSConfig)
for name, group := range l2tpCfg.Groups {
if len(group.LNS) == 0 {
return configError{Field: fmt.Sprintf("ppp.l2tp.groups.%s.lns", name), Err: fmt.Errorf("at least one LNS is required")}
}
for _, lns := range group.LNS {
if lns.Address == "" {
return configError{Field: fmt.Sprintf("ppp.l2tp.groups.%s.lns.address", name), Err: fmt.Errorf("LNS address is required")}
}
w := lns.Weight
if w <= 0 {
w = 1
}
lbGroups[name] = append(lbGroups[name], l2tp.LNSConfig{
Address: lns.Address,
Secret: lns.Secret,
Weight: w,
})
}
}

// Validate realms
if len(l2tpCfg.Realms) == 0 {
return configError{Field: "ppp.l2tp.realms", Err: fmt.Errorf("at least one realm rule is required")}
}
var realmRules []l2tp.RealmRule
for _, realm := range l2tpCfg.Realms {
if realm.Pattern == "" {
return configError{Field: "ppp.l2tp.realms.pattern", Err: fmt.Errorf("realm pattern is required")}
}
if _, ok := l2tpCfg.Groups[realm.Group]; !ok {
return configError{Field: "ppp.l2tp.realms.group", Err: fmt.Errorf("realm references unknown group %q", realm.Group)}
}
realmRules = append(realmRules, l2tp.RealmRule{
Pattern: realm.Pattern,
Group: realm.Group,
})
}

helloInterval := time.Duration(l2tpCfg.HelloInterval) * time.Second

tm := l2tp.NewTunnelManager(l2tpCfg.Hostname, helloInterval, logger)
rr := l2tp.NewRealmRouter(realmRules)
lb := l2tp.NewLoadBalancer(lbGroups)

logger.Info("PPP enabled (L2TP mode)",
zap.String("hostname", l2tpCfg.Hostname),
zap.Int("helloInterval", l2tpCfg.HelloInterval),
zap.Int("groups", len(l2tpCfg.Groups)),
zap.Int("realms", len(l2tpCfg.Realms)))

hyConfig.PPPRequestHandler = &pppbridge.L2TPPPPHandler{
TunnelManager: tm,
RealmRouter: rr,
LoadBalancer: lb,
Logger: logger,
}
return nil
}
Loading