mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-06-07 19:54:56 +03:00
Add SSH inbound, log level. Update MTPROXY. Fixes
This commit is contained in:
@@ -1,12 +1,16 @@
|
||||
-s dir
|
||||
--name sing-box
|
||||
--name sing-box-extended
|
||||
--category net
|
||||
--license GPL-3.0-or-later
|
||||
--description "The universal proxy platform."
|
||||
--description "The universal proxy platform (extended)."
|
||||
--url "https://sing-box.sagernet.org/"
|
||||
--maintainer "nekohasekai <contact-git@sekai.icu>"
|
||||
--no-deb-generate-changes
|
||||
|
||||
--provides sing-box
|
||||
--conflicts sing-box
|
||||
--replaces sing-box
|
||||
|
||||
--config-files /etc/config/sing-box
|
||||
--config-files /etc/sing-box/config.json
|
||||
|
||||
|
||||
12
.github/build_openwrt_apk.sh
vendored
12
.github/build_openwrt_apk.sh
vendored
@@ -27,10 +27,7 @@ fi
|
||||
PROJECT=$(cd "$(dirname "$0")/.."; pwd)
|
||||
|
||||
# Convert version to APK format:
|
||||
# 1.13.0-beta.8 -> 1.13.0_beta8-r0
|
||||
# 1.13.0-rc.3 -> 1.13.0_rc3-r0
|
||||
# 1.13.0 -> 1.13.0-r0
|
||||
APK_VERSION=$(echo "$VERSION" | sed -E 's/-([a-z]+)\.([0-9]+)/_\1\2/')
|
||||
APK_VERSION=$(echo "$VERSION" | sed -E 's/-([a-z]+)\.([0-9]+)/_\1\2/' | sed -E 's/-[a-z]+-/./g')
|
||||
APK_VERSION="${APK_VERSION}-r0"
|
||||
|
||||
ROOT_DIR=$(mktemp -d)
|
||||
@@ -78,15 +75,16 @@ done < "$PACKAGES_DIR/.conffiles" > "$PACKAGES_DIR/.conffiles_static"
|
||||
|
||||
# Build APK
|
||||
apk --root "$APK_ROOT_DIR" mkpkg \
|
||||
--info "name:sing-box" \
|
||||
--info "name:sing-box-extended" \
|
||||
--info "version:${APK_VERSION}" \
|
||||
--info "description:The universal proxy platform." \
|
||||
--info "description:The universal proxy platform (extended)." \
|
||||
--info "arch:${ARCHITECTURE}" \
|
||||
--info "license:GPL-3.0-or-later" \
|
||||
--info "origin:sing-box" \
|
||||
--info "origin:sing-box-extended" \
|
||||
--info "url:https://sing-box.sagernet.org/" \
|
||||
--info "maintainer:nekohasekai <contact-git@sekai.icu>" \
|
||||
--info "depends:ca-bundle kmod-inet-diag kmod-tun firewall4 kmod-nft-queue" \
|
||||
--info "provides:sing-box" \
|
||||
--info "provider-priority:100" \
|
||||
--script "pre-deinstall:${PROJECT}/release/config/openwrt.prerm" \
|
||||
--files "$ROOT_DIR" \
|
||||
|
||||
53
.github/build_openwrt_packages.sh
vendored
Executable file
53
.github/build_openwrt_packages.sh
vendored
Executable file
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e -o pipefail
|
||||
|
||||
VERSION="$1"
|
||||
TARGET="$2"
|
||||
BINARY_PATH="$3"
|
||||
|
||||
PROJECT=$(cd "$(dirname "$0")/.."; pwd)
|
||||
DIST="$PROJECT/dist"
|
||||
|
||||
case "$TARGET" in
|
||||
linux_amd64*) ARCHITECTURES="x86_64" ;;
|
||||
linux_arm64*) ARCHITECTURES="aarch64_cortex-a53 aarch64_cortex-a72 aarch64_cortex-a76 aarch64_generic" ;;
|
||||
linux_386*softfloat) ARCHITECTURES="i386_pentium-mmx" ;;
|
||||
linux_386*) ARCHITECTURES="i386_pentium4" ;;
|
||||
linux_arm_7*) ARCHITECTURES="arm_cortex-a5_vfpv4 arm_cortex-a7_neon-vfpv4 arm_cortex-a7_vfpv4 arm_cortex-a8_vfpv3 arm_cortex-a9_neon arm_cortex-a9_vfpv3-d16 arm_cortex-a15_neon-vfpv4" ;;
|
||||
linux_arm_6*) ARCHITECTURES="arm_arm1176jzf-s_vfp" ;;
|
||||
linux_arm_5*) ARCHITECTURES="arm_arm926ej-s arm_cortex-a7 arm_cortex-a9 arm_fa526 arm_xscale" ;;
|
||||
linux_mips64_*) ARCHITECTURES="mips64_mips64r2 mips64_octeonplus" ;;
|
||||
linux_mips64le*) ARCHITECTURES="mips64el_mips64r2" ;;
|
||||
linux_mipsle*hardfloat) ARCHITECTURES="mipsel_24kc_24kf" ;;
|
||||
linux_mipsle*) ARCHITECTURES="mipsel_24kc mipsel_74kc mipsel_mips32" ;;
|
||||
linux_mips_*) ARCHITECTURES="mips_24kc mips_4kec mips_mips32" ;;
|
||||
linux_riscv64*) ARCHITECTURES="riscv64_generic" ;;
|
||||
linux_loong64*) ARCHITECTURES="loongarch64_generic" ;;
|
||||
*) echo "Unknown target: $TARGET"; exit 1 ;;
|
||||
esac
|
||||
|
||||
PKG_VERSION="${VERSION//-/\~}"
|
||||
|
||||
for ARCH in $ARCHITECTURES; do
|
||||
cp "$PROJECT/.fpm_openwrt" "$PROJECT/.fpm"
|
||||
fpm -t deb \
|
||||
-v "$PKG_VERSION" \
|
||||
-p "$DIST/_openwrt_tmp.deb" \
|
||||
--architecture all \
|
||||
"$BINARY_PATH=/usr/bin/sing-box"
|
||||
rm -f "$PROJECT/.fpm"
|
||||
|
||||
bash "$PROJECT/.github/deb2ipk.sh" \
|
||||
"$ARCH" \
|
||||
"$DIST/_openwrt_tmp.deb" \
|
||||
"$DIST/sing-box-extended_${VERSION}_openwrt_${ARCH}.ipk"
|
||||
rm -f "$DIST/_openwrt_tmp.deb"
|
||||
|
||||
if command -v apk &>/dev/null; then
|
||||
bash "$PROJECT/.github/build_openwrt_apk.sh" \
|
||||
"$ARCH" "$VERSION" "$BINARY_PATH" \
|
||||
"$DIST/sing-box-extended_${VERSION}_openwrt_${ARCH}.apk"
|
||||
fi
|
||||
|
||||
echo "Built: sing-box-extended_${VERSION}_openwrt_${ARCH} (.ipk/.apk)"
|
||||
done
|
||||
@@ -393,6 +393,49 @@ builds:
|
||||
- android_arm64
|
||||
- android_386
|
||||
- android_amd64
|
||||
- id: openwrt
|
||||
<<: *template
|
||||
hooks:
|
||||
post:
|
||||
- cmd: bash .github/build_openwrt_packages.sh "{{ .Version }}" "{{ .Target }}" "{{ .Path }}"
|
||||
targets:
|
||||
- linux_amd64_v1
|
||||
- linux_arm64
|
||||
- linux_386
|
||||
- linux_arm_7
|
||||
- linux_arm_6
|
||||
- linux_riscv64
|
||||
- linux_loong64
|
||||
- id: openwrt-mips
|
||||
<<: *template
|
||||
tags:
|
||||
- with_gvisor
|
||||
- with_quic
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_utls
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_ccm
|
||||
- with_ocm
|
||||
- with_openvpn
|
||||
- with_trusttunnel
|
||||
- with_sudoku
|
||||
- badlinkname
|
||||
- tfogo_checklinkname0
|
||||
hooks:
|
||||
post:
|
||||
- cmd: bash .github/build_openwrt_packages.sh "{{ .Version }}" "{{ .Target }}" "{{ .Path }}"
|
||||
targets:
|
||||
- linux_arm_5
|
||||
- linux_mips_softfloat
|
||||
- linux_mips64_softfloat
|
||||
- linux_mipsle_softfloat
|
||||
- linux_mipsle_hardfloat
|
||||
- linux_mips64le
|
||||
upx:
|
||||
- enabled: true
|
||||
ids:
|
||||
@@ -471,6 +514,12 @@ archives:
|
||||
- compressed-mips
|
||||
- compressed-android
|
||||
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ if and .Mips (not (eq .Mips "hardfloat")) }}-{{ .Mips }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}-compressed'
|
||||
- id: archive-openwrt
|
||||
<<: *template
|
||||
builds:
|
||||
- openwrt
|
||||
- openwrt-mips
|
||||
name_template: '{{ .ProjectName }}-{{ .Version }}-openwrt-{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ if and .Mips (not (eq .Mips "hardfloat")) }}-{{ .Mips }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
|
||||
source:
|
||||
enabled: false
|
||||
name_template: '{{ .ProjectName }}-{{ .Version }}.source'
|
||||
@@ -496,5 +545,5 @@ release:
|
||||
- archive-naive-purego-windows-amd64
|
||||
- archive-naive-purego-windows-arm64
|
||||
- archive-compressed
|
||||
- package
|
||||
- archive-openwrt
|
||||
skip_upload: true
|
||||
|
||||
3
Makefile
3
Makefile
@@ -27,7 +27,6 @@ CRONET_GO_PATH ?= $(shell pwd)/cronet-go
|
||||
.PHONY: test release docs build
|
||||
|
||||
build:
|
||||
export GOTOOLCHAIN=local && \
|
||||
go build $(MAIN_PARAMS) $(MAIN)
|
||||
|
||||
build_admin_panel:
|
||||
@@ -94,6 +93,8 @@ release: build_admin_panel build_naive
|
||||
mkdir dist/release
|
||||
mv dist/*.tar.gz \
|
||||
dist/*.zip \
|
||||
dist/*.ipk \
|
||||
dist/*.apk \
|
||||
dist/release
|
||||
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release
|
||||
./codeberg-release.sh --replace --draft --prerelease -p 5 "v${VERSION}" dist/release
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
[](LICENSE)
|
||||
[](go.mod)
|
||||
[](https://codeberg.org/shtorm-7/sing-box-extended)
|
||||
[](https://t.me/sing_box_extended)
|
||||
|
||||
Sing-box with extended features.
|
||||
|
||||
@@ -16,6 +17,7 @@ Sing-box with extended features.
|
||||
- **OpenVPN** — OpenVPN client with tls-auth, tls-crypt and tls-crypt-v2 support
|
||||
- **TrustTunnel** — AdGuard's obfuscated VPN protocol, indistinguishable from HTTPS traffic
|
||||
- **Sudoku** — Traffic obfuscation protocol based on 4×4 Sudoku puzzles with low-entropy fingerprints
|
||||
- **SSH** — SSH client and server with certificate authentication and upstream fallback
|
||||
- **VPN** — Routed tunnel over any TCP sing-box protocol
|
||||
- **Bond** — Link aggregation for increasing throughput
|
||||
- **Fallback** — Outbound group with priority-based switching
|
||||
|
||||
@@ -233,7 +233,7 @@ func (m *Manager) Remove(tag string) error {
|
||||
if m.defaultOutbound == outbound {
|
||||
if len(m.outbounds) > 0 {
|
||||
m.defaultOutbound = m.outbounds[0]
|
||||
m.logger.Info("updated default outbound to ", m.defaultOutbound.Tag())
|
||||
m.logger.Notice("updated default outbound to ", m.defaultOutbound.Tag())
|
||||
} else {
|
||||
m.defaultOutbound = nil
|
||||
}
|
||||
@@ -303,7 +303,7 @@ func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.
|
||||
if tag == m.defaultTag || (m.defaultTag == "" && m.defaultOutbound == nil) {
|
||||
m.defaultOutbound = outbound
|
||||
if m.started {
|
||||
m.logger.Info("updated default outbound to ", outbound.Tag())
|
||||
m.logger.Notice("updated default outbound to ", outbound.Tag())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
4
box.go
4
box.go
@@ -458,7 +458,7 @@ func (s *Box) PreStart() error {
|
||||
s.Close()
|
||||
return err
|
||||
}
|
||||
s.logger.Info("sing-box pre-started (", F.Seconds(time.Since(s.createdAt).Seconds()), "s)")
|
||||
s.logger.Notice("sing-box pre-started (", F.Seconds(time.Since(s.createdAt).Seconds()), "s)")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -477,7 +477,7 @@ func (s *Box) Start() error {
|
||||
s.Close()
|
||||
return err
|
||||
}
|
||||
s.logger.Info("sing-box started (", F.Seconds(time.Since(s.createdAt).Seconds()), "s)")
|
||||
s.logger.Notice("sing-box started (", F.Seconds(time.Since(s.createdAt).Seconds()), "s)")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -179,6 +179,7 @@ func run() error {
|
||||
for {
|
||||
osSignal := <-osSignals
|
||||
if osSignal == syscall.SIGHUP {
|
||||
log.Notice("received SIGHUP, reloading...")
|
||||
err = check()
|
||||
if err != nil {
|
||||
log.Error(E.Cause(err, "reload service"))
|
||||
|
||||
@@ -77,7 +77,7 @@ func (l *Listener) ListenTCP() (net.Listener, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l.logger.Info("tcp server started at ", tcpListener.Addr())
|
||||
l.logger.Notice("tcp server started at ", tcpListener.Addr())
|
||||
l.tcpListener = tcpListener
|
||||
return tcpListener, err
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ func (l *Listener) ListenUDP() (net.PacketConn, error) {
|
||||
}
|
||||
l.udpConn = udpConn.(*net.UDPConn)
|
||||
l.udpAddr = bindAddr
|
||||
l.logger.Info("udp server started at ", udpConn.LocalAddr())
|
||||
l.logger.Notice("udp server started at ", udpConn.LocalAddr())
|
||||
return udpConn, err
|
||||
}
|
||||
|
||||
|
||||
@@ -164,7 +164,7 @@ func (c *STDServerConfig) certificateUpdated(path string) error {
|
||||
config.Certificates = []tls.Certificate{keyPair}
|
||||
c.config = config
|
||||
c.access.Unlock()
|
||||
c.logger.Info("reloaded TLS certificate")
|
||||
c.logger.Notice("reloaded TLS certificate")
|
||||
} else if common.Contains(c.clientCertificatePath, path) {
|
||||
clientCertificateCA := x509.NewCertPool()
|
||||
var reloaded bool
|
||||
@@ -188,7 +188,7 @@ func (c *STDServerConfig) certificateUpdated(path string) error {
|
||||
config.ClientCAs = clientCertificateCA
|
||||
c.config = config
|
||||
c.access.Unlock()
|
||||
c.logger.Info("reloaded client certificates")
|
||||
c.logger.Notice("reloaded client certificates")
|
||||
} else if path == c.echKeyPath {
|
||||
echKey, err := os.ReadFile(c.echKeyPath)
|
||||
if err != nil {
|
||||
@@ -198,7 +198,7 @@ func (c *STDServerConfig) certificateUpdated(path string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.logger.Info("reloaded ECH keys")
|
||||
c.logger.Notice("reloaded ECH keys")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -20,13 +20,14 @@ const (
|
||||
type LogLevel int32
|
||||
|
||||
const (
|
||||
LogLevel_PANIC LogLevel = 0
|
||||
LogLevel_FATAL LogLevel = 1
|
||||
LogLevel_ERROR LogLevel = 2
|
||||
LogLevel_WARN LogLevel = 3
|
||||
LogLevel_INFO LogLevel = 4
|
||||
LogLevel_DEBUG LogLevel = 5
|
||||
LogLevel_TRACE LogLevel = 6
|
||||
LogLevel_PANIC LogLevel = 0
|
||||
LogLevel_FATAL LogLevel = 1
|
||||
LogLevel_ERROR LogLevel = 2
|
||||
LogLevel_WARN LogLevel = 3
|
||||
LogLevel_NOTICE LogLevel = 4
|
||||
LogLevel_INFO LogLevel = 5
|
||||
LogLevel_DEBUG LogLevel = 6
|
||||
LogLevel_TRACE LogLevel = 7
|
||||
)
|
||||
|
||||
// Enum value maps for LogLevel.
|
||||
@@ -36,18 +37,20 @@ var (
|
||||
1: "FATAL",
|
||||
2: "ERROR",
|
||||
3: "WARN",
|
||||
4: "INFO",
|
||||
5: "DEBUG",
|
||||
6: "TRACE",
|
||||
4: "NOTICE",
|
||||
5: "INFO",
|
||||
6: "DEBUG",
|
||||
7: "TRACE",
|
||||
}
|
||||
LogLevel_value = map[string]int32{
|
||||
"PANIC": 0,
|
||||
"FATAL": 1,
|
||||
"ERROR": 2,
|
||||
"WARN": 3,
|
||||
"INFO": 4,
|
||||
"DEBUG": 5,
|
||||
"TRACE": 6,
|
||||
"PANIC": 0,
|
||||
"FATAL": 1,
|
||||
"ERROR": 2,
|
||||
"WARN": 3,
|
||||
"NOTICE": 4,
|
||||
"INFO": 5,
|
||||
"DEBUG": 6,
|
||||
"TRACE": 7,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -59,9 +59,10 @@ enum LogLevel {
|
||||
FATAL = 1;
|
||||
ERROR = 2;
|
||||
WARN = 3;
|
||||
INFO = 4;
|
||||
DEBUG = 5;
|
||||
TRACE = 6;
|
||||
NOTICE = 4;
|
||||
INFO = 5;
|
||||
DEBUG = 6;
|
||||
TRACE = 7;
|
||||
}
|
||||
|
||||
message Log {
|
||||
|
||||
@@ -186,7 +186,7 @@ func (t *Transport) updateServers() error {
|
||||
return E.Cause(err, "dhcp: prepare interface")
|
||||
}
|
||||
|
||||
t.logger.Info("dhcp: query DNS servers on ", iface.Name)
|
||||
t.logger.Notice("dhcp: query DNS servers on ", iface.Name)
|
||||
fetchCtx, cancel := context.WithTimeout(t.ctx, C.DHCPTimeout)
|
||||
err = t.fetchServers0(fetchCtx, iface)
|
||||
cancel()
|
||||
@@ -303,7 +303,7 @@ func (t *Transport) recreateServers(iface *control.Interface, dhcpPacket *dhcpv4
|
||||
return M.SocksaddrFrom(M.AddrFromIP(it), 53)
|
||||
})
|
||||
if len(serverAddrs) > 0 && !slices.Equal(t.servers, serverAddrs) {
|
||||
t.logger.Info("dhcp: updated DNS servers from ", iface.Name, ": [", strings.Join(common.Map(serverAddrs, M.Socksaddr.String), ","), "], search: [", strings.Join(t.search, ","), "]")
|
||||
t.logger.Notice("dhcp: updated DNS servers from ", iface.Name, ": [", strings.Join(common.Map(serverAddrs, M.Socksaddr.String), ","), "], search: [", strings.Join(t.search, ","), "]")
|
||||
}
|
||||
t.servers = serverAddrs
|
||||
return nil
|
||||
|
||||
@@ -217,7 +217,7 @@ func (m *TransportManager) Remove(tag string) error {
|
||||
return E.New("default server cannot be fakeip")
|
||||
}
|
||||
m.defaultTransport = nextTransport
|
||||
m.logger.Info("updated default server to ", m.defaultTransport.Tag())
|
||||
m.logger.Notice("updated default server to ", m.defaultTransport.Tag())
|
||||
} else {
|
||||
m.defaultTransport = nil
|
||||
}
|
||||
@@ -287,7 +287,7 @@ func (m *TransportManager) Create(ctx context.Context, logger log.ContextLogger,
|
||||
}
|
||||
m.defaultTransport = transport
|
||||
if m.started {
|
||||
m.logger.Info("updated default server to ", transport.Tag())
|
||||
m.logger.Notice("updated default server to ", transport.Tag())
|
||||
}
|
||||
}
|
||||
if transport.Type() == C.DNSTypeFakeIP {
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"tag": "my-manager",
|
||||
"database": {
|
||||
"driver": "sqlite",
|
||||
"dsn": "file:manager.db?_pragma=foreign_keys(on)&_pragma=journal_mode(wal)&_pragma=busy_timeout(5000)" // also supported Postgresql
|
||||
"dsn": "file:manager.db?_pragma=foreign_keys(on)&_pragma=journal_mode(wal)&_pragma=busy_timeout(5000)&_time_format=sqlite" // also supported Postgresql
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
{
|
||||
"type": "masque",
|
||||
"tag": "masque-out",
|
||||
"system": false,
|
||||
"name": "masque0",
|
||||
"use_http2": false,
|
||||
"use_ipv6": false,
|
||||
"profile": {
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
"type": "mieru",
|
||||
"tag": "mieru-in",
|
||||
"listen_port": 27017,
|
||||
"listen_ports": [
|
||||
"27017-27019"
|
||||
],
|
||||
"transport": "TCP",
|
||||
"users": [
|
||||
{
|
||||
|
||||
@@ -31,7 +31,15 @@
|
||||
"packet_encoding": "",
|
||||
"transport": {
|
||||
"type": "mkcp",
|
||||
"mtu": 1500
|
||||
"mtu": 1350, // 576-1460
|
||||
"tti": 50, // 10-100, ms
|
||||
"uplink_capacity": 12, // MB/s
|
||||
"downlink_capacity": 100, // MB/s
|
||||
"congestion": false,
|
||||
"read_buffer_size": 1, // MB
|
||||
"write_buffer_size": 1, // MB
|
||||
"header_type": "none", // none, srtp, utp, wechat-video, dtls, wireguard
|
||||
"seed": "password"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -24,7 +24,15 @@
|
||||
],
|
||||
"transport": {
|
||||
"type": "mkcp",
|
||||
"mtu": 1500
|
||||
"mtu": 1350, // 576-1460
|
||||
"tti": 50, // 10-100, ms
|
||||
"uplink_capacity": 12, // MB/s
|
||||
"downlink_capacity": 100, // MB/s
|
||||
"congestion": false,
|
||||
"read_buffer_size": 1, // MB
|
||||
"write_buffer_size": 1, // MB
|
||||
"header_type": "none", // none, srtp, utp, wechat-video, dtls, wireguard
|
||||
"seed": "password"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -26,9 +26,9 @@
|
||||
"concurrency": 8192,
|
||||
// domain_fronting_port is a port we use to connect to a fronting domain.
|
||||
"domain_fronting_port": 443,
|
||||
// domain_fronting_ip is an IP address to use when connecting to the fronting
|
||||
// domain instead of resolving the hostname from the secret via DNS.
|
||||
"domain_fronting_ip": "",
|
||||
// domain_fronting_host is the address (IP or hostname) to use when connecting
|
||||
// to the fronting domain instead of resolving the hostname from the secret via DNS.
|
||||
"domain_fronting_host": "",
|
||||
// domain_fronting_proxy_protocol is used if communication between upstream
|
||||
// endpoint and sing-box supports proxy protocol.
|
||||
"domain_fronting_proxy_protocol": false,
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
{
|
||||
"type": "openvpn",
|
||||
"tag": "openvpn-out",
|
||||
"system": false,
|
||||
"name": "openvpn0",
|
||||
"servers": [
|
||||
{
|
||||
"server": "vpn.example.com",
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
{
|
||||
"type": "openvpn",
|
||||
"tag": "openvpn-out",
|
||||
"system": false,
|
||||
"name": "openvpn0",
|
||||
"servers": [
|
||||
{
|
||||
"server": "vpn.example.com",
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
{
|
||||
"type": "openvpn",
|
||||
"tag": "openvpn-out",
|
||||
"system": false,
|
||||
"name": "openvpn0",
|
||||
"servers": [
|
||||
{
|
||||
"server": "vpn.example.com",
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
{
|
||||
"type": "openvpn",
|
||||
"tag": "openvpn-out",
|
||||
"system": false,
|
||||
"name": "openvpn0",
|
||||
"servers": [
|
||||
{
|
||||
"server": "vpn.example.com",
|
||||
|
||||
52
examples/ssh/client.json
Normal file
52
examples/ssh/client.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "error"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "default"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "mixed",
|
||||
"tag": "mixed-in",
|
||||
"listen_port": 7897
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
},
|
||||
{
|
||||
"type": "ssh",
|
||||
"tag": "ssh-out",
|
||||
"server": "example.com",
|
||||
"server_port": 2222,
|
||||
"user": "user",
|
||||
// Authentication: password or private key
|
||||
"password": "password",
|
||||
"private_key": [
|
||||
"-----BEGIN OPENSSH PRIVATE KEY-----\n...\n-----END OPENSSH PRIVATE KEY-----"
|
||||
],
|
||||
// or: "private_key_path": "/path/to/id_ed25519",
|
||||
"private_key_passphrase": "",
|
||||
// Pin server host key (optional)
|
||||
"host_key": [
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA..."
|
||||
],
|
||||
"host_key_algorithms": ["ssh-ed25519"],
|
||||
"client_version": "SSH-2.0-OpenSSH_9.6"
|
||||
// Dial Fields
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"final": "ssh-out",
|
||||
"default_domain_resolver": "default",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
76
examples/ssh/server.json
Normal file
76
examples/ssh/server.json
Normal file
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "info"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "default"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "ssh",
|
||||
"tag": "ssh-in",
|
||||
"listen": "0.0.0.0",
|
||||
"listen_port": 2222,
|
||||
"host_key": [
|
||||
"-----BEGIN OPENSSH PRIVATE KEY-----\n...\n-----END OPENSSH PRIVATE KEY-----"
|
||||
],
|
||||
// or: "host_key_path": ["/etc/sing-box/ssh_host_ed25519_key"],
|
||||
"server_version": "SSH-2.0-OpenSSH_9.6",
|
||||
"max_auth_tries": 3,
|
||||
"users": [
|
||||
{
|
||||
"name": "user1",
|
||||
"password": "password1"
|
||||
},
|
||||
{
|
||||
"name": "user2",
|
||||
"authorized_keys": [
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... user2@host"
|
||||
]
|
||||
}
|
||||
],
|
||||
"fallback": {
|
||||
"server": "10.0.0.2",
|
||||
"server_port": 22,
|
||||
"ca": {
|
||||
"private_key": [
|
||||
"-----BEGIN OPENSSH PRIVATE KEY-----\n...\n-----END OPENSSH PRIVATE KEY-----"
|
||||
],
|
||||
// or: "private_key_path": "/etc/sing-box/ca_key",
|
||||
"private_key_passphrase": ""
|
||||
},
|
||||
// Optional: separate CA for issuing upstream certs (defaults to ca)
|
||||
"issue_ca": {
|
||||
"private_key": [
|
||||
"-----BEGIN OPENSSH PRIVATE KEY-----\n...\n-----END OPENSSH PRIVATE KEY-----"
|
||||
],
|
||||
// or: "private_key_path": "/etc/sing-box/issue_ca_key",
|
||||
"private_key_passphrase": ""
|
||||
},
|
||||
"host_key": [
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... upstream-host-key"
|
||||
],
|
||||
// or: "host_key_path": ["/etc/sing-box/upstream_host_key.pub"],
|
||||
"host_key_algorithms": ["ssh-ed25519"],
|
||||
"client_version": "SSH-2.0-OpenSSH_9.6"
|
||||
// Dial Fields
|
||||
}
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"final": "direct",
|
||||
"default_domain_resolver": "default",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ func updateExternalUI(server *Server) func(w http.ResponseWriter, r *http.Reques
|
||||
render.JSON(w, r, newError(err.Error()))
|
||||
return
|
||||
}
|
||||
server.logger.Info("updated external UI")
|
||||
server.logger.Notice("updated external UI")
|
||||
render.JSON(w, r, render.M{"status": "ok"})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +177,7 @@ func (s *Server) Start(stage adapter.StartStage) error {
|
||||
if err != nil {
|
||||
return E.Cause(err, "external controller listen error")
|
||||
}
|
||||
s.logger.Info("restful api listening at ", listener.Addr())
|
||||
s.logger.Notice("restful api listening at ", listener.Addr())
|
||||
go func() {
|
||||
err = s.httpServer.Serve(listener)
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
@@ -234,7 +234,7 @@ func (s *Server) SetMode(newMode string) {
|
||||
s.logger.Error(E.Cause(err, "save mode"))
|
||||
}
|
||||
}
|
||||
s.logger.Info("updated mode: ", newMode)
|
||||
s.logger.Notice("updated mode: ", newMode)
|
||||
}
|
||||
|
||||
func (s *Server) HistoryStorage() adapter.URLTestHistoryStorage {
|
||||
|
||||
@@ -56,7 +56,7 @@ func (s *Server) Start(stage adapter.StartStage) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.logger.Info("grpc server started at ", listener.Addr())
|
||||
s.logger.Notice("grpc server started at ", listener.Addr())
|
||||
s.tcpListener = listener
|
||||
go func() {
|
||||
err = s.grpcServer.Serve(listener)
|
||||
|
||||
4
go.mod
4
go.mod
@@ -229,10 +229,10 @@ replace github.com/ameshkov/dnscrypt/v2 => github.com/shtorm-7/dnscrypt/v2 v2.4.
|
||||
|
||||
replace github.com/sagernet/sing-vmess => github.com/shtorm-7/sing-vmess v0.2.7-extended-1.0.0
|
||||
|
||||
replace github.com/dolonet/mtg-multi => github.com/shtorm-7/mtg-multi v1.8.0-extended-1.0.1
|
||||
replace github.com/dolonet/mtg-multi => github.com/shtorm-7/mtg-multi v1.11.0-extended-1.0.0
|
||||
|
||||
replace github.com/Diniboy1123/connect-ip-go => github.com/shtorm-7/connect-ip-go v1.0.0-extended-1.0.0
|
||||
|
||||
replace github.com/shtorm-7/go-cache/v2 => github.com/shtorm-7/go-cache/v2 v2.1.0-extended-1.1.0
|
||||
|
||||
replace github.com/sagernet/sing => github.com/shtorm-7/sing v0.8.10-extended-1.0.0
|
||||
replace github.com/sagernet/sing => github.com/shtorm-7/sing v0.8.10-extended-1.1.0
|
||||
|
||||
8
go.sum
8
go.sum
@@ -379,10 +379,10 @@ github.com/shtorm-7/dnscrypt/v2 v2.4.0-extended-1.0.0 h1:e5s7RKBd2rIPR0StbvZ2vTV
|
||||
github.com/shtorm-7/dnscrypt/v2 v2.4.0-extended-1.0.0/go.mod h1:WpEFV2uhebXb8Jhes/5/fSdpmhGV8TL22RDaeWwV6hI=
|
||||
github.com/shtorm-7/go-cache/v2 v2.1.0-extended-1.1.0 h1:PLZ/YHqnApPx13wt6MX3ItqESp4ueBr1tGSi0bEGqYw=
|
||||
github.com/shtorm-7/go-cache/v2 v2.1.0-extended-1.1.0/go.mod h1:Ek4yz5OK6stwhLKgLsRRYDI+FA+ZWvRJiWLjsi/vMM4=
|
||||
github.com/shtorm-7/mtg-multi v1.8.0-extended-1.0.1 h1:UeJkrCJJmIjTBywErVMx7fCSoBf4gh6QgT9bp9o1ajM=
|
||||
github.com/shtorm-7/mtg-multi v1.8.0-extended-1.0.1/go.mod h1:3rvdhwdPABkwKBdvgMt3VwMn9uSq8hpoHRezZ5jRJU0=
|
||||
github.com/shtorm-7/sing v0.8.10-extended-1.0.0 h1:mAkyycCQOzCttPOR5fcHkJaZvXMQXeu3mbEfr8D+7A8=
|
||||
github.com/shtorm-7/sing v0.8.10-extended-1.0.0/go.mod h1:olXxWQNqRW/l2Q6JI3b2Qmz8iQnIFlOeeH8bx6JhgUA=
|
||||
github.com/shtorm-7/mtg-multi v1.11.0-extended-1.0.0 h1:iBLll4ZZG8ULQcHWs6gGslZWtBN72Zo1zjySzMVHF7g=
|
||||
github.com/shtorm-7/mtg-multi v1.11.0-extended-1.0.0/go.mod h1:3rvdhwdPABkwKBdvgMt3VwMn9uSq8hpoHRezZ5jRJU0=
|
||||
github.com/shtorm-7/sing v0.8.10-extended-1.1.0 h1:P4JL2cugjvEvnYu8tMmpR30SE1qsS45RcnNEwzDz5as=
|
||||
github.com/shtorm-7/sing v0.8.10-extended-1.1.0/go.mod h1:olXxWQNqRW/l2Q6JI3b2Qmz8iQnIFlOeeH8bx6JhgUA=
|
||||
github.com/shtorm-7/sing-mux v0.3.4-extended-1.0.0 h1:a5OoXr3e2ACbM6vDIaaGL44IdHQ6wPjcSoU13vfC0Sw=
|
||||
github.com/shtorm-7/sing-mux v0.3.4-extended-1.0.0/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk=
|
||||
github.com/shtorm-7/sing-vmess v0.2.7-extended-1.0.0 h1:WVheKmQH5hSQbJU1ZTKthKSutkTLWSb2hp4JuQhJBow=
|
||||
|
||||
@@ -81,6 +81,7 @@ func InboundRegistry() *inbound.Registry {
|
||||
vless.RegisterInbound(registry)
|
||||
anytls.RegisterInbound(registry)
|
||||
mieru.RegisterInbound(registry)
|
||||
ssh.RegisterInbound(registry)
|
||||
|
||||
bond.RegisterInbound(registry)
|
||||
failover.RegisterInbound(registry)
|
||||
|
||||
@@ -39,6 +39,10 @@ func Info(args ...any) {
|
||||
std.Info(args...)
|
||||
}
|
||||
|
||||
func Notice(args ...any) {
|
||||
std.Notice(args...)
|
||||
}
|
||||
|
||||
func Warn(args ...any) {
|
||||
std.Warn(args...)
|
||||
}
|
||||
@@ -67,6 +71,10 @@ func InfoContext(ctx context.Context, args ...any) {
|
||||
std.InfoContext(ctx, args...)
|
||||
}
|
||||
|
||||
func NoticeContext(ctx context.Context, args ...any) {
|
||||
std.NoticeContext(ctx, args...)
|
||||
}
|
||||
|
||||
func WarnContext(ctx context.Context, args ...any) {
|
||||
std.WarnContext(ctx, args...)
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ func (f Formatter) Format(ctx context.Context, level Level, tag string, message
|
||||
levelString = aurora.White(levelString).String()
|
||||
case LevelInfo:
|
||||
levelString = aurora.Cyan(levelString).String()
|
||||
case LevelNotice:
|
||||
levelString = aurora.Green(levelString).String()
|
||||
case LevelWarn:
|
||||
levelString = aurora.Yellow(levelString).String()
|
||||
case LevelError, LevelFatal, LevelPanic:
|
||||
@@ -97,6 +99,8 @@ func (f Formatter) FormatWithSimple(ctx context.Context, level Level, tag string
|
||||
levelString = aurora.White(levelString).String()
|
||||
case LevelInfo:
|
||||
levelString = aurora.Cyan(levelString).String()
|
||||
case LevelNotice:
|
||||
levelString = aurora.Green(levelString).String()
|
||||
case LevelWarn:
|
||||
levelString = aurora.Yellow(levelString).String()
|
||||
case LevelError, LevelFatal, LevelPanic:
|
||||
|
||||
@@ -11,6 +11,7 @@ const (
|
||||
LevelFatal
|
||||
LevelError
|
||||
LevelWarn
|
||||
LevelNotice
|
||||
LevelInfo
|
||||
LevelDebug
|
||||
LevelTrace
|
||||
@@ -24,6 +25,8 @@ func FormatLevel(level Level) string {
|
||||
return "debug"
|
||||
case LevelInfo:
|
||||
return "info"
|
||||
case LevelNotice:
|
||||
return "notice"
|
||||
case LevelWarn:
|
||||
return "warn"
|
||||
case LevelError:
|
||||
@@ -45,6 +48,8 @@ func ParseLevel(level string) (Level, error) {
|
||||
return LevelDebug, nil
|
||||
case "info":
|
||||
return LevelInfo, nil
|
||||
case "notice":
|
||||
return LevelNotice, nil
|
||||
case "warn", "warning":
|
||||
return LevelWarn, nil
|
||||
case "error":
|
||||
|
||||
@@ -47,6 +47,9 @@ func (f *nopFactory) Debug(args ...any) {
|
||||
func (f *nopFactory) Info(args ...any) {
|
||||
}
|
||||
|
||||
func (f *nopFactory) Notice(args ...any) {
|
||||
}
|
||||
|
||||
func (f *nopFactory) Warn(args ...any) {
|
||||
}
|
||||
|
||||
@@ -68,6 +71,9 @@ func (f *nopFactory) DebugContext(ctx context.Context, args ...any) {
|
||||
func (f *nopFactory) InfoContext(ctx context.Context, args ...any) {
|
||||
}
|
||||
|
||||
func (f *nopFactory) NoticeContext(ctx context.Context, args ...any) {
|
||||
}
|
||||
|
||||
func (f *nopFactory) WarnContext(ctx context.Context, args ...any) {
|
||||
}
|
||||
|
||||
|
||||
@@ -154,6 +154,10 @@ func (l *observableLogger) Info(args ...any) {
|
||||
l.InfoContext(context.Background(), args...)
|
||||
}
|
||||
|
||||
func (l *observableLogger) Notice(args ...any) {
|
||||
l.NoticeContext(context.Background(), args...)
|
||||
}
|
||||
|
||||
func (l *observableLogger) Warn(args ...any) {
|
||||
l.WarnContext(context.Background(), args...)
|
||||
}
|
||||
@@ -182,6 +186,10 @@ func (l *observableLogger) InfoContext(ctx context.Context, args ...any) {
|
||||
l.Log(ctx, LevelInfo, args)
|
||||
}
|
||||
|
||||
func (l *observableLogger) NoticeContext(ctx context.Context, args ...any) {
|
||||
l.Log(ctx, LevelNotice, args)
|
||||
}
|
||||
|
||||
func (l *observableLogger) WarnContext(ctx context.Context, args ...any) {
|
||||
l.Log(ctx, LevelWarn, args)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,6 @@ type ManagerServiceDatabase struct {
|
||||
}
|
||||
|
||||
type ManagerServiceOptions struct {
|
||||
Inbounds []string `json:"inbounds"`
|
||||
Inbounds []string `json:"inbounds"`
|
||||
Database ManagerServiceDatabase `json:"database"`
|
||||
}
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
package option
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
|
||||
"github.com/sagernet/sing/common/json/badoption"
|
||||
)
|
||||
|
||||
type MASQUEOutboundOptions struct {
|
||||
DialerOptions
|
||||
UseHTTP2 bool `json:"use_http2,omitempty"`
|
||||
UseIPv6 bool `json:"use_ipv6,omitempty"`
|
||||
Profile CloudflareProfile `json:"profile,omitempty"`
|
||||
UDPTimeout badoption.Duration `json:"udp_timeout,omitempty"`
|
||||
UDPKeepalivePeriod badoption.Duration `json:"udp_keepalive_period,omitempty"`
|
||||
UDPInitialPacketSize uint16 `json:"udp_initial_packet_size,omitempty"`
|
||||
ReconnectDelay badoption.Duration `json:"reconnect_delay,omitempty"`
|
||||
System bool `json:"system,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
AllowedIPs badoption.Listable[netip.Prefix] `json:"allowed_ips,omitempty"`
|
||||
UseHTTP2 bool `json:"use_http2,omitempty"`
|
||||
UseIPv6 bool `json:"use_ipv6,omitempty"`
|
||||
Profile CloudflareProfile `json:"profile,omitempty"`
|
||||
UDPTimeout badoption.Duration `json:"udp_timeout,omitempty"`
|
||||
UDPKeepalivePeriod badoption.Duration `json:"udp_keepalive_period,omitempty"`
|
||||
UDPInitialPacketSize uint16 `json:"udp_initial_packet_size,omitempty"`
|
||||
ReconnectDelay badoption.Duration `json:"reconnect_delay,omitempty"`
|
||||
MASQUEOutboundTLSOptionsContainer
|
||||
}
|
||||
|
||||
|
||||
@@ -15,10 +15,11 @@ type MieruOutboundOptions struct {
|
||||
|
||||
type MieruInboundOptions struct {
|
||||
ListenOptions
|
||||
Users []MieruUser `json:"users,omitempty"`
|
||||
Transport string `json:"transport,omitempty"`
|
||||
TrafficPattern string `json:"traffic_pattern,omitempty"`
|
||||
UserHintIsMandatory bool `json:"user_hint_is_mandatory,omitempty"`
|
||||
ListenPorts badoption.Listable[string] `json:"listen_ports,omitempty"`
|
||||
Users []MieruUser `json:"users,omitempty"`
|
||||
Transport string `json:"transport,omitempty"`
|
||||
TrafficPattern string `json:"traffic_pattern,omitempty"`
|
||||
UserHintIsMandatory bool `json:"user_hint_is_mandatory,omitempty"`
|
||||
}
|
||||
|
||||
type MieruUser struct {
|
||||
|
||||
@@ -11,7 +11,7 @@ type MTProxyInboundOptions struct {
|
||||
Users []MTProxyUser `json:"users,omitempty"`
|
||||
Concurrency uint `json:"concurrency,omitempty"`
|
||||
DomainFrontingPort uint `json:"domain_fronting_port,omitempty"`
|
||||
DomainFrontingIP string `json:"domain_fronting_ip,omitempty"`
|
||||
DomainFrontingHost string `json:"domain_fronting_host,omitempty"`
|
||||
DomainFrontingProxyProtocol bool `json:"domain_fronting_proxy_protocol,omitempty"`
|
||||
PreferIP string `json:"prefer_ip,omitempty"`
|
||||
AutoUpdate bool `json:"auto_update,omitempty"`
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package option
|
||||
|
||||
type NodeServiceOptions struct {
|
||||
UUID string `json:"uuid"`
|
||||
Inbounds []string `json:"inbounds"`
|
||||
ConnectionLimiters []string `json:"connection_limiters"`
|
||||
BandwidthLimiters []string `json:"bandwidth_limiters"`
|
||||
TrafficLimiters []string `json:"traffic_limiters"`
|
||||
RateLimiters []string `json:"rate_limiters"`
|
||||
Manager string `json:"manager"`
|
||||
UUID string `json:"uuid"`
|
||||
Inbounds []string `json:"inbounds"`
|
||||
ConnectionLimiters []string `json:"connection_limiters"`
|
||||
BandwidthLimiters []string `json:"bandwidth_limiters"`
|
||||
TrafficLimiters []string `json:"traffic_limiters"`
|
||||
RateLimiters []string `json:"rate_limiters"`
|
||||
Manager string `json:"manager"`
|
||||
}
|
||||
|
||||
@@ -1,38 +1,45 @@
|
||||
package option
|
||||
|
||||
import "github.com/sagernet/sing/common/json/badoption"
|
||||
import (
|
||||
"net/netip"
|
||||
|
||||
"github.com/sagernet/sing/common/json/badoption"
|
||||
)
|
||||
|
||||
type OpenVPNOutboundOptions struct {
|
||||
DialerOptions
|
||||
Servers []ServerOptions `json:"servers"`
|
||||
Proto string `json:"proto,omitempty"`
|
||||
Cipher string `json:"cipher,omitempty"`
|
||||
Auth string `json:"auth,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
TLSCrypt string `json:"tls_crypt,omitempty"`
|
||||
TLSCryptPath string `json:"tls_crypt_path,omitempty"`
|
||||
TLSCryptV2 bool `json:"tls_crypt_v2,omitempty"`
|
||||
TLSAuth string `json:"tls_auth,omitempty"`
|
||||
TLSAuthPath string `json:"tls_auth_path,omitempty"`
|
||||
KeyDirection int `json:"key_direction,omitempty"`
|
||||
ReconnectDelay badoption.Duration `json:"reconnect_delay,omitempty"`
|
||||
PingInterval badoption.Duration `json:"ping_interval,omitempty"`
|
||||
System bool `json:"system,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
AllowedIPs badoption.Listable[netip.Prefix] `json:"allowed_ips,omitempty"`
|
||||
Servers []ServerOptions `json:"servers"`
|
||||
Proto string `json:"proto,omitempty"`
|
||||
Cipher string `json:"cipher,omitempty"`
|
||||
Auth string `json:"auth,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
TLSCrypt string `json:"tls_crypt,omitempty"`
|
||||
TLSCryptPath string `json:"tls_crypt_path,omitempty"`
|
||||
TLSCryptV2 bool `json:"tls_crypt_v2,omitempty"`
|
||||
TLSAuth string `json:"tls_auth,omitempty"`
|
||||
TLSAuthPath string `json:"tls_auth_path,omitempty"`
|
||||
KeyDirection int `json:"key_direction,omitempty"`
|
||||
ReconnectDelay badoption.Duration `json:"reconnect_delay,omitempty"`
|
||||
PingInterval badoption.Duration `json:"ping_interval,omitempty"`
|
||||
OpenVPNOutboundTLSOptionsContainer
|
||||
}
|
||||
|
||||
type OpenVPNTLSOptions struct {
|
||||
Certificate string `json:"certificate,omitempty"`
|
||||
CertificatePath string `json:"certificate_path,omitempty"`
|
||||
Key string `json:"key,omitempty"`
|
||||
KeyPath string `json:"key_path,omitempty"`
|
||||
CA string `json:"ca,omitempty"`
|
||||
CAPath string `json:"ca_path,omitempty"`
|
||||
CipherSuites badoption.Listable[string] `json:"cipher_suites,omitempty"`
|
||||
VerifyX509Name string `json:"verify_x509_name,omitempty"`
|
||||
VerifyX509NameMode string `json:"verify_x509_name_mode,omitempty"`
|
||||
KernelTx bool `json:"kernel_tx,omitempty"`
|
||||
KernelRx bool `json:"kernel_rx,omitempty"`
|
||||
Certificate string `json:"certificate,omitempty"`
|
||||
CertificatePath string `json:"certificate_path,omitempty"`
|
||||
Key string `json:"key,omitempty"`
|
||||
KeyPath string `json:"key_path,omitempty"`
|
||||
CA string `json:"ca,omitempty"`
|
||||
CAPath string `json:"ca_path,omitempty"`
|
||||
CipherSuites badoption.Listable[string] `json:"cipher_suites,omitempty"`
|
||||
VerifyX509Name string `json:"verify_x509_name,omitempty"`
|
||||
VerifyX509NameMode string `json:"verify_x509_name_mode,omitempty"`
|
||||
KernelTx bool `json:"kernel_tx,omitempty"`
|
||||
KernelRx bool `json:"kernel_rx,omitempty"`
|
||||
}
|
||||
|
||||
type OpenVPNOutboundTLSOptionsContainer struct {
|
||||
|
||||
@@ -2,6 +2,39 @@ package option
|
||||
|
||||
import "github.com/sagernet/sing/common/json/badoption"
|
||||
|
||||
type SSHInboundOptions struct {
|
||||
ListenOptions
|
||||
Users []SSHUser `json:"users,omitempty"`
|
||||
HostKey badoption.Listable[string] `json:"host_key,omitempty"`
|
||||
HostKeyPath badoption.Listable[string] `json:"host_key_path,omitempty"`
|
||||
ServerVersion string `json:"server_version,omitempty"`
|
||||
MaxAuthTries int `json:"max_auth_tries,omitempty"`
|
||||
Fallback *SSHFallbackServerOptions `json:"fallback,omitempty"`
|
||||
}
|
||||
|
||||
type SSHFallbackServerOptions struct {
|
||||
DialerOptions
|
||||
ServerOptions
|
||||
CA *SSHCAOptions `json:"ca,omitempty"`
|
||||
IssueCA *SSHCAOptions `json:"issue_ca,omitempty"`
|
||||
HostKey badoption.Listable[string] `json:"host_key,omitempty"`
|
||||
HostKeyPath badoption.Listable[string] `json:"host_key_path,omitempty"`
|
||||
HostKeyAlgorithms badoption.Listable[string] `json:"host_key_algorithms,omitempty"`
|
||||
ClientVersion string `json:"client_version,omitempty"`
|
||||
}
|
||||
|
||||
type SSHCAOptions struct {
|
||||
PrivateKey badoption.Listable[string] `json:"private_key,omitempty"`
|
||||
PrivateKeyPath string `json:"private_key_path,omitempty"`
|
||||
PrivateKeyPassphrase string `json:"private_key_passphrase,omitempty"`
|
||||
}
|
||||
|
||||
type SSHUser struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
AuthorizedKeys badoption.Listable[string] `json:"authorized_keys,omitempty"`
|
||||
}
|
||||
|
||||
type SSHOutboundOptions struct {
|
||||
DialerOptions
|
||||
ServerOptions
|
||||
|
||||
@@ -22,7 +22,7 @@ func NewBondedConn(conns []net.Conn, downloadRatios, uploadRatios []uint8) *bond
|
||||
conns: conns,
|
||||
downloadRatios: downloadRatios,
|
||||
uploadRatios: uploadRatios,
|
||||
readBuffer: bytes.NewBuffer(make([]byte, 0, 65536)),
|
||||
readBuffer: bytes.NewBuffer(make([]byte, 0, 4096)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,13 +37,13 @@ type failoverConn struct {
|
||||
func NewFailoverConn(ctx context.Context, conn net.Conn, dial dial, onClose func()) *failoverConn {
|
||||
var writeBuffers [BufferSize][]byte
|
||||
for i := range BufferSize {
|
||||
writeBuffers[i] = make([]byte, 0, 1000)
|
||||
writeBuffers[i] = make([]byte, 0, 1024)
|
||||
}
|
||||
return &failoverConn{
|
||||
Conn: conn,
|
||||
ctx: ctx,
|
||||
dial: dial,
|
||||
readBuffer: bytes.NewBuffer(make([]byte, 0, 1000)),
|
||||
readBuffer: bytes.NewBuffer(make([]byte, 0, 1024)),
|
||||
writeBuffers: writeBuffers,
|
||||
onClose: onClose,
|
||||
}
|
||||
|
||||
@@ -194,8 +194,6 @@ type bwConnEntry struct {
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
|
||||
|
||||
type ManagerBandwidthStrategy struct {
|
||||
strategies map[string]BandwidthStrategy
|
||||
conns map[string][]*bwConnEntry
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/adapter/outbound"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/common/onclose"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-box/route"
|
||||
@@ -173,8 +173,6 @@ func (h *Outbound) GetStrategy() ConnectionStrategy {
|
||||
return h.strategy
|
||||
}
|
||||
|
||||
|
||||
|
||||
func connChecker(ctx context.Context, closeFunc func() error) {
|
||||
<-ctx.Done()
|
||||
closeFunc()
|
||||
|
||||
@@ -76,8 +76,6 @@ type connEntry struct {
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
|
||||
|
||||
type ManagerTrafficStrategy struct {
|
||||
strategies map[string]TrafficStrategy
|
||||
conns map[string][]*connEntry
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing-box/adapter/outbound"
|
||||
"github.com/sagernet/sing-box/common/cloudflare"
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
@@ -18,6 +17,7 @@ import (
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-box/transport/masque"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
@@ -136,11 +136,19 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
|
||||
ctx,
|
||||
logger,
|
||||
masque.TunnelOptions{
|
||||
System: options.System,
|
||||
Name: options.Name,
|
||||
CreateDialer: func(interfaceName string) N.Dialer {
|
||||
return common.Must1(dialer.NewDefault(ctx, option.DialerOptions{
|
||||
BindInterface: interfaceName,
|
||||
}))
|
||||
},
|
||||
Dialer: outboundDialer,
|
||||
Address: []netip.Prefix{
|
||||
netip.MustParsePrefix(appConfig.IPv4 + "/32"),
|
||||
netip.MustParsePrefix(appConfig.IPv6 + "/128"),
|
||||
},
|
||||
AllowedAddress: options.AllowedIPs,
|
||||
Endpoint: endpoint,
|
||||
TLSConfig: tlsConfig,
|
||||
UseHTTP2: options.UseHTTP2,
|
||||
|
||||
@@ -87,7 +87,7 @@ func (h *Inbound) Start(stage adapter.StartStage) error {
|
||||
return fmt.Errorf("failed to start mieru server: %w", err)
|
||||
}
|
||||
|
||||
h.logger.Info("mieru server is started")
|
||||
h.logger.Notice("mieru server is started")
|
||||
go h.acceptLoop()
|
||||
return nil
|
||||
}
|
||||
@@ -275,14 +275,21 @@ func buildMieruServerConfig(_ context.Context, options option.MieruInboundOption
|
||||
transportProtocol = mierupb.TransportProtocol_UDP.Enum()
|
||||
}
|
||||
|
||||
if options.ListenOptions.ListenPort == 0 {
|
||||
return nil, nil, E.New("listen_port must be set")
|
||||
if options.ListenOptions.ListenPort == 0 && len(options.ListenPorts) == 0 {
|
||||
return nil, nil, E.New("either listen_port or listen_ports must be set")
|
||||
}
|
||||
portBindings := []*mierupb.PortBinding{
|
||||
{
|
||||
var portBindings []*mierupb.PortBinding
|
||||
if options.ListenOptions.ListenPort != 0 {
|
||||
portBindings = append(portBindings, &mierupb.PortBinding{
|
||||
Port: proto.Int32(int32(options.ListenOptions.ListenPort)),
|
||||
Protocol: transportProtocol,
|
||||
},
|
||||
})
|
||||
}
|
||||
for _, pr := range options.ListenPorts {
|
||||
portBindings = append(portBindings, &mierupb.PortBinding{
|
||||
PortRange: proto.String(pr),
|
||||
Protocol: transportProtocol,
|
||||
})
|
||||
}
|
||||
|
||||
var users []*mierupb.User
|
||||
|
||||
@@ -53,7 +53,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
|
||||
if err := c.Start(); err != nil {
|
||||
return nil, fmt.Errorf("failed to start mieru client: %w", err)
|
||||
}
|
||||
logger.InfoContext(ctx, "mieru client is started")
|
||||
logger.NoticeContext(ctx, "mieru client is started")
|
||||
|
||||
return &Outbound{
|
||||
Adapter: outbound.NewAdapterWithDialerOptions(C.TypeMieru, tag, []string{N.NetworkTCP, N.NetworkUDP}, options.DialerOptions),
|
||||
|
||||
@@ -63,7 +63,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
||||
Secrets: secrets,
|
||||
Concurrency: options.GetConcurrency(),
|
||||
DomainFrontingPort: options.GetDomainFrontingPort(),
|
||||
DomainFrontingIP: options.DomainFrontingIP,
|
||||
DomainFrontingHost: options.DomainFrontingHost,
|
||||
DomainFrontingProxyProtocol: options.DomainFrontingProxyProtocol,
|
||||
PreferIP: options.GetPreferIP(),
|
||||
AutoUpdate: options.AutoUpdate,
|
||||
|
||||
@@ -227,7 +227,7 @@ func (h *Outbound) Start(stage adapter.StartStage) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h.logger.Info("NaiveProxy started, version: ", h.client.Engine().Version())
|
||||
h.logger.Notice("NaiveProxy started, version: ", h.client.Engine().Version())
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ package openvpn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
"net"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/adapter/outbound"
|
||||
@@ -90,10 +90,18 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
|
||||
logger: logger,
|
||||
}
|
||||
tunnel, err := ovpn.NewTunnel(ctx, logger, ovpn.TunnelOptions{
|
||||
System: options.System,
|
||||
Name: options.Name,
|
||||
CreateDialer: func(interfaceName string) N.Dialer {
|
||||
return common.Must1(dialer.NewDefault(ctx, option.DialerOptions{
|
||||
BindInterface: interfaceName,
|
||||
}))
|
||||
},
|
||||
Dialer: outboundDialer,
|
||||
Servers: options.Servers,
|
||||
TLSConfig: tlsConfig,
|
||||
Config: clientConfig,
|
||||
AllowedAddress: options.AllowedIPs,
|
||||
ReconnectDelay: time.Duration(options.ReconnectDelay),
|
||||
PingInterval: time.Duration(options.PingInterval),
|
||||
})
|
||||
|
||||
91
protocol/ssh/certificate.go
Normal file
91
protocol/ssh/certificate.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
func parseCAKey(options *option.SSHCAOptions) (ssh.Signer, error) {
|
||||
var keyData []byte
|
||||
var err error
|
||||
if len(options.PrivateKey) > 0 {
|
||||
keyData = []byte(strings.Join(options.PrivateKey, "\n"))
|
||||
} else if options.PrivateKeyPath != "" {
|
||||
keyData, err = os.ReadFile(os.ExpandEnv(options.PrivateKeyPath))
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "read CA private key")
|
||||
}
|
||||
} else {
|
||||
return nil, E.New("missing CA private key")
|
||||
}
|
||||
if options.PrivateKeyPassphrase == "" {
|
||||
return ssh.ParsePrivateKey(keyData)
|
||||
}
|
||||
return ssh.ParsePrivateKeyWithPassphrase(keyData, []byte(options.PrivateKeyPassphrase))
|
||||
}
|
||||
|
||||
func verifyCertificate(signer ssh.Signer, metadata ssh.ConnMetadata, key ssh.PublicKey) bool {
|
||||
if signer == nil {
|
||||
return false
|
||||
}
|
||||
certificate, ok := key.(*ssh.Certificate)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
checker := &ssh.CertChecker{
|
||||
IsUserAuthority: func(auth ssh.PublicKey) bool {
|
||||
return bytes.Equal(auth.Marshal(), signer.PublicKey().Marshal())
|
||||
},
|
||||
}
|
||||
if !checker.IsUserAuthority(certificate.SignatureKey) {
|
||||
return false
|
||||
}
|
||||
return checker.CheckCert(metadata.User(), certificate) == nil
|
||||
}
|
||||
|
||||
func issueCertificate(signer ssh.Signer, user string) (ssh.Signer, error) {
|
||||
_, privateKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ephemeral, err := ssh.NewSignerFromSigner(privateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
now := time.Now()
|
||||
certificate := &ssh.Certificate{
|
||||
Key: ephemeral.PublicKey(),
|
||||
Serial: uint64(now.UnixNano()),
|
||||
CertType: ssh.UserCert,
|
||||
KeyId: user,
|
||||
ValidPrincipals: []string{user},
|
||||
ValidAfter: uint64(now.Add(-1 * time.Minute).Unix()),
|
||||
ValidBefore: uint64(now.Add(5 * time.Minute).Unix()),
|
||||
Permissions: ssh.Permissions{
|
||||
Extensions: map[string]string{
|
||||
"permit-pty": "",
|
||||
"permit-port-forwarding": "",
|
||||
"permit-agent-forwarding": "",
|
||||
"permit-X11-forwarding": "",
|
||||
"permit-user-rc": "",
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := certificate.SignCert(rand.Reader, signer); err != nil {
|
||||
return nil, E.Cause(err, "sign certificate")
|
||||
}
|
||||
certSigner, err := ssh.NewCertSigner(certificate, ephemeral)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create certificate signer")
|
||||
}
|
||||
return certSigner, nil
|
||||
}
|
||||
322
protocol/ssh/fallback.go
Normal file
322
protocol/ssh/fallback.go
Normal file
@@ -0,0 +1,322 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
"github.com/sagernet/sing-box/common/onclose"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
var _ Service = (*Fallback)(nil)
|
||||
|
||||
type Fallback struct {
|
||||
Service
|
||||
ctx context.Context
|
||||
logger logger.ContextLogger
|
||||
dialer N.Dialer
|
||||
serverAddr M.Socksaddr
|
||||
clientVersion string
|
||||
mainSigner ssh.Signer
|
||||
issueSigner ssh.Signer
|
||||
hostKeys []ssh.PublicKey
|
||||
keyAlgorithms []string
|
||||
pending map[string]*upstreamConn
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
type upstreamConn struct {
|
||||
conn net.Conn
|
||||
client ssh.Conn
|
||||
channels <-chan ssh.NewChannel
|
||||
requests <-chan *ssh.Request
|
||||
}
|
||||
|
||||
func NewFallback(ctx context.Context, logger logger.ContextLogger, inner Service, options *option.SSHFallbackServerOptions) (*Fallback, error) {
|
||||
serverAddr := options.Build()
|
||||
if serverAddr.Port == 0 {
|
||||
serverAddr.Port = 22
|
||||
}
|
||||
if !serverAddr.Addr.IsValid() && serverAddr.Fqdn == "" {
|
||||
return nil, E.New("missing upstream server address")
|
||||
}
|
||||
upstreamDialer, err := dialer.New(ctx, options.DialerOptions, serverAddr.IsFqdn())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fallback := &Fallback{
|
||||
Service: inner,
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
dialer: upstreamDialer,
|
||||
serverAddr: serverAddr,
|
||||
clientVersion: options.ClientVersion,
|
||||
keyAlgorithms: options.HostKeyAlgorithms,
|
||||
pending: make(map[string]*upstreamConn),
|
||||
}
|
||||
if fallback.clientVersion == "" {
|
||||
fallback.clientVersion = "SSH-2.0-OpenSSH_9.6"
|
||||
}
|
||||
if options.CA != nil {
|
||||
signer, err := parseCAKey(options.CA)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "parse CA")
|
||||
}
|
||||
fallback.mainSigner = signer
|
||||
}
|
||||
if options.IssueCA != nil {
|
||||
signer, err := parseCAKey(options.IssueCA)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "parse issue CA")
|
||||
}
|
||||
fallback.issueSigner = signer
|
||||
}
|
||||
if fallback.issueSigner == nil && fallback.mainSigner != nil {
|
||||
fallback.issueSigner = fallback.mainSigner
|
||||
}
|
||||
for _, hostKey := range options.HostKey {
|
||||
key, _, _, _, err := ssh.ParseAuthorizedKey([]byte(hostKey))
|
||||
if err != nil {
|
||||
return nil, E.New("parse upstream host key ", hostKey)
|
||||
}
|
||||
fallback.hostKeys = append(fallback.hostKeys, key)
|
||||
}
|
||||
for _, hostKeyPath := range options.HostKeyPath {
|
||||
content, err := os.ReadFile(os.ExpandEnv(hostKeyPath))
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "read upstream host key ", hostKeyPath)
|
||||
}
|
||||
key, _, _, _, err := ssh.ParseAuthorizedKey(content)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "parse upstream host key ", hostKeyPath)
|
||||
}
|
||||
fallback.hostKeys = append(fallback.hostKeys, key)
|
||||
}
|
||||
return fallback, nil
|
||||
}
|
||||
|
||||
func (f *Fallback) PasswordCallback(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) {
|
||||
if permissions, err := f.Service.PasswordCallback(conn, password); err == nil {
|
||||
return permissions, nil
|
||||
}
|
||||
if err := f.dial(string(conn.SessionID()), conn.User(), ssh.Password(string(password))); err != nil {
|
||||
return nil, E.Cause(err, "upstream authentication failed for user ", conn.User())
|
||||
}
|
||||
return &ssh.Permissions{Extensions: map[string]string{"user": conn.User(), "fallback": "1"}}, nil
|
||||
}
|
||||
|
||||
func (f *Fallback) PublicKeyCallback(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
|
||||
if permissions, err := f.Service.PublicKeyCallback(conn, key); err == nil {
|
||||
return permissions, nil
|
||||
}
|
||||
if verifyCertificate(f.mainSigner, conn, key) {
|
||||
signer, err := issueCertificate(f.issueSigner, conn.User())
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "upstream authentication failed for user ", conn.User())
|
||||
}
|
||||
if err := f.dial(string(conn.SessionID()), conn.User(), ssh.PublicKeys(signer)); err != nil {
|
||||
return nil, E.Cause(err, "upstream authentication failed for user ", conn.User())
|
||||
}
|
||||
return &ssh.Permissions{Extensions: map[string]string{"user": conn.User(), "fallback": "1"}}, nil
|
||||
}
|
||||
return nil, E.New("public key authentication failed for user ", conn.User())
|
||||
}
|
||||
|
||||
func (f *Fallback) Handle(ctx context.Context, serverConn *ssh.ServerConn, channels <-chan ssh.NewChannel, requests <-chan *ssh.Request, metadata adapter.InboundContext, user string) {
|
||||
if serverConn.Permissions == nil || serverConn.Permissions.Extensions["fallback"] != "1" {
|
||||
f.Service.Handle(ctx, serverConn, channels, requests, metadata, user)
|
||||
return
|
||||
}
|
||||
sessionID := string(serverConn.SessionID())
|
||||
f.mtx.Lock()
|
||||
upstream := f.pending[sessionID]
|
||||
delete(f.pending, sessionID)
|
||||
f.mtx.Unlock()
|
||||
if upstream == nil {
|
||||
serverConn.Close()
|
||||
return
|
||||
}
|
||||
f.logger.InfoContext(ctx, "[", user, "] forwarded SSH connection from ", metadata.Source)
|
||||
go proxyDownstreamRequests(requests, upstream.client)
|
||||
go proxyGlobalRequests(upstream.requests, serverConn)
|
||||
go func() {
|
||||
for newChannel := range upstream.channels {
|
||||
go proxyChannel(newChannel, serverConn)
|
||||
}
|
||||
}()
|
||||
var wg sync.WaitGroup
|
||||
for newChannel := range channels {
|
||||
wg.Go(func() {
|
||||
proxyChannel(newChannel, upstream.client)
|
||||
})
|
||||
}
|
||||
wg.Wait()
|
||||
upstream.client.Close()
|
||||
upstream.conn.Close()
|
||||
serverConn.Close()
|
||||
}
|
||||
|
||||
func (f *Fallback) Close() error {
|
||||
f.mtx.Lock()
|
||||
connections := make([]net.Conn, 0, len(f.pending))
|
||||
for id, upstream := range f.pending {
|
||||
if upstream != nil {
|
||||
connections = append(connections, upstream.conn)
|
||||
}
|
||||
delete(f.pending, id)
|
||||
}
|
||||
f.mtx.Unlock()
|
||||
for _, conn := range connections {
|
||||
conn.Close()
|
||||
}
|
||||
return f.Service.Close()
|
||||
}
|
||||
|
||||
func (f *Fallback) dial(sessionID string, user string, auth ssh.AuthMethod) error {
|
||||
f.mtx.Lock()
|
||||
if _, attempted := f.pending[sessionID]; attempted {
|
||||
f.mtx.Unlock()
|
||||
return E.New("fallback already attempted")
|
||||
}
|
||||
f.pending[sessionID] = nil
|
||||
f.mtx.Unlock()
|
||||
conn, err := f.dialer.DialContext(f.ctx, N.NetworkTCP, f.serverAddr)
|
||||
if err != nil {
|
||||
f.mtx.Lock()
|
||||
delete(f.pending, sessionID)
|
||||
f.mtx.Unlock()
|
||||
return err
|
||||
}
|
||||
conn = onclose.NewConn(conn, func() {
|
||||
f.mtx.Lock()
|
||||
delete(f.pending, sessionID)
|
||||
f.mtx.Unlock()
|
||||
})
|
||||
config := &ssh.ClientConfig{
|
||||
User: user,
|
||||
Auth: []ssh.AuthMethod{auth},
|
||||
ClientVersion: f.clientVersion,
|
||||
HostKeyAlgorithms: f.keyAlgorithms,
|
||||
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
|
||||
if len(f.hostKeys) == 0 {
|
||||
return nil
|
||||
}
|
||||
serverKey := key.Marshal()
|
||||
for _, hostKey := range f.hostKeys {
|
||||
if bytes.Equal(serverKey, hostKey.Marshal()) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return E.New("upstream host key mismatch, server sent ", key.Type(), " ", base64.StdEncoding.EncodeToString(serverKey))
|
||||
},
|
||||
Timeout: C.TCPTimeout,
|
||||
}
|
||||
client, channels, requests, err := ssh.NewClientConn(conn, f.serverAddr.String(), config)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return err
|
||||
}
|
||||
f.mtx.Lock()
|
||||
f.pending[sessionID] = &upstreamConn{conn: conn, client: client, channels: channels, requests: requests}
|
||||
f.mtx.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func proxyChannel(newChannel ssh.NewChannel, target ssh.Conn) {
|
||||
targetChannel, targetRequests, err := target.OpenChannel(newChannel.ChannelType(), newChannel.ExtraData())
|
||||
if err != nil {
|
||||
if openErr, ok := err.(*ssh.OpenChannelError); ok {
|
||||
newChannel.Reject(openErr.Reason, openErr.Message)
|
||||
} else {
|
||||
newChannel.Reject(ssh.ConnectionFailed, err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
sourceChannel, sourceRequests, err := newChannel.Accept()
|
||||
if err != nil {
|
||||
targetChannel.Close()
|
||||
return
|
||||
}
|
||||
go proxyChannelRequests(sourceRequests, targetChannel)
|
||||
go io.Copy(targetChannel.Stderr(), sourceChannel.Stderr())
|
||||
go io.Copy(sourceChannel.Stderr(), targetChannel.Stderr())
|
||||
go func() {
|
||||
io.Copy(targetChannel, sourceChannel)
|
||||
targetChannel.CloseWrite()
|
||||
}()
|
||||
go func() {
|
||||
io.Copy(sourceChannel, targetChannel)
|
||||
sourceChannel.CloseWrite()
|
||||
}()
|
||||
proxyChannelRequests(targetRequests, sourceChannel)
|
||||
sourceChannel.Close()
|
||||
targetChannel.Close()
|
||||
}
|
||||
|
||||
func proxyGlobalRequests(requests <-chan *ssh.Request, target ssh.Conn) {
|
||||
for request := range requests {
|
||||
if request.Type == "hostkeys-00@openssh.com" {
|
||||
if request.WantReply {
|
||||
request.Reply(false, nil)
|
||||
}
|
||||
continue
|
||||
}
|
||||
ok, payload, err := target.SendRequest(request.Type, request.WantReply, request.Payload)
|
||||
if request.WantReply {
|
||||
if err != nil {
|
||||
request.Reply(false, nil)
|
||||
} else {
|
||||
request.Reply(ok, payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func proxyDownstreamRequests(requests <-chan *ssh.Request, target ssh.Conn) {
|
||||
for request := range requests {
|
||||
switch request.Type {
|
||||
case "no-more-sessions@openssh.com", "hostkeys-prove-00@openssh.com":
|
||||
if request.WantReply {
|
||||
request.Reply(false, nil)
|
||||
}
|
||||
continue
|
||||
}
|
||||
ok, payload, err := target.SendRequest(request.Type, request.WantReply, request.Payload)
|
||||
if request.WantReply {
|
||||
if err != nil {
|
||||
request.Reply(false, nil)
|
||||
} else {
|
||||
request.Reply(ok, payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func proxyChannelRequests(requests <-chan *ssh.Request, target ssh.Channel) {
|
||||
for request := range requests {
|
||||
ok, err := target.SendRequest(request.Type, request.WantReply, request.Payload)
|
||||
if request.WantReply {
|
||||
if err != nil {
|
||||
request.Reply(false, nil)
|
||||
} else {
|
||||
request.Reply(ok, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
152
protocol/ssh/inbound.go
Normal file
152
protocol/ssh/inbound.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/adapter/inbound"
|
||||
"github.com/sagernet/sing-box/common/listener"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
func RegisterInbound(registry *inbound.Registry) {
|
||||
inbound.Register[option.SSHInboundOptions](registry, C.TypeSSH, NewInbound)
|
||||
}
|
||||
|
||||
var _ adapter.TCPInjectableInbound = (*Inbound)(nil)
|
||||
|
||||
type Inbound struct {
|
||||
inbound.Adapter
|
||||
logger logger.ContextLogger
|
||||
listener *listener.Listener
|
||||
serverConfig *ssh.ServerConfig
|
||||
service Service
|
||||
}
|
||||
|
||||
func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SSHInboundOptions) (adapter.Inbound, error) {
|
||||
if len(options.Users) == 0 && options.Fallback == nil {
|
||||
return nil, E.New("missing users")
|
||||
}
|
||||
inbound := &Inbound{
|
||||
Adapter: inbound.NewAdapter(C.TypeSSH, tag),
|
||||
logger: logger,
|
||||
}
|
||||
defaultService := newService(router, logger, options.Users)
|
||||
if options.Fallback != nil {
|
||||
fallback, err := NewFallback(ctx, logger, defaultService, options.Fallback)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
inbound.service = fallback
|
||||
} else {
|
||||
inbound.service = defaultService
|
||||
}
|
||||
serverVersion := options.ServerVersion
|
||||
if serverVersion == "" {
|
||||
serverVersion = "SSH-2.0-OpenSSH_9.6"
|
||||
}
|
||||
serverConfig := &ssh.ServerConfig{
|
||||
ServerVersion: serverVersion,
|
||||
MaxAuthTries: options.MaxAuthTries,
|
||||
PasswordCallback: inbound.service.PasswordCallback,
|
||||
PublicKeyCallback: inbound.service.PublicKeyCallback,
|
||||
}
|
||||
var hostKeys []ssh.Signer
|
||||
for _, hostKey := range options.HostKey {
|
||||
signer, err := ssh.ParsePrivateKey([]byte(hostKey))
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "parse host key")
|
||||
}
|
||||
hostKeys = append(hostKeys, signer)
|
||||
}
|
||||
for _, hostKeyPath := range options.HostKeyPath {
|
||||
content, err := os.ReadFile(os.ExpandEnv(hostKeyPath))
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "read host key ", hostKeyPath)
|
||||
}
|
||||
signer, err := ssh.ParsePrivateKey(content)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "parse host key ", hostKeyPath)
|
||||
}
|
||||
hostKeys = append(hostKeys, signer)
|
||||
}
|
||||
if len(hostKeys) == 0 {
|
||||
_, privateKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "generate host key")
|
||||
}
|
||||
signer, err := ssh.NewSignerFromSigner(privateKey)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "generate host key")
|
||||
}
|
||||
hostKeys = append(hostKeys, signer)
|
||||
}
|
||||
for _, hostKey := range hostKeys {
|
||||
serverConfig.AddHostKey(hostKey)
|
||||
}
|
||||
inbound.serverConfig = serverConfig
|
||||
inbound.listener = listener.New(listener.Options{
|
||||
Context: ctx,
|
||||
Logger: logger,
|
||||
Network: []string{N.NetworkTCP},
|
||||
Listen: options.ListenOptions,
|
||||
ConnectionHandler: inbound,
|
||||
})
|
||||
return inbound, nil
|
||||
}
|
||||
|
||||
func (h *Inbound) Start(stage adapter.StartStage) error {
|
||||
if stage != adapter.StartStateStart {
|
||||
return nil
|
||||
}
|
||||
return h.listener.Start()
|
||||
}
|
||||
|
||||
func (h *Inbound) Close() error {
|
||||
return E.Errors(h.service.Close(), h.listener.Close())
|
||||
}
|
||||
|
||||
func (h *Inbound) UpdateUsers(users []option.SSHUser) {
|
||||
h.service.UpdateUsers(users)
|
||||
}
|
||||
|
||||
func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
||||
metadata.Inbound = h.Tag()
|
||||
metadata.InboundType = h.Type()
|
||||
serverConn, channels, requests, err := ssh.NewServerConn(conn, h.serverConfig)
|
||||
if err != nil {
|
||||
N.CloseOnHandshakeFailure(conn, onClose, err)
|
||||
if E.IsClosedOrCanceled(err) {
|
||||
h.logger.DebugContext(ctx, "connection closed: ", err)
|
||||
} else {
|
||||
h.logger.DebugContext(ctx, E.Cause(err, "process connection from ", metadata.Source))
|
||||
}
|
||||
return
|
||||
}
|
||||
var user string
|
||||
if serverConn.Permissions != nil {
|
||||
user = serverConn.Permissions.Extensions["user"]
|
||||
}
|
||||
if user == "" {
|
||||
user = serverConn.User()
|
||||
}
|
||||
if user != "" {
|
||||
metadata.User = user
|
||||
}
|
||||
go func() {
|
||||
serverConn.Wait()
|
||||
conn.Close()
|
||||
}()
|
||||
h.service.Handle(ctx, serverConn, channels, requests, metadata, user)
|
||||
}
|
||||
165
protocol/ssh/service.go
Normal file
165
protocol/ssh/service.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/listener"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common/bufio/deadline"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
PasswordCallback(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error)
|
||||
PublicKeyCallback(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error)
|
||||
UpdateUsers(users []option.SSHUser)
|
||||
Handle(ctx context.Context, serverConn *ssh.ServerConn, channels <-chan ssh.NewChannel, requests <-chan *ssh.Request, metadata adapter.InboundContext, user string)
|
||||
Close() error
|
||||
}
|
||||
|
||||
var _ Service = (*service)(nil)
|
||||
|
||||
type service struct {
|
||||
router adapter.ConnectionRouterEx
|
||||
logger logger.ContextLogger
|
||||
listener *listener.Listener
|
||||
users []option.SSHUser
|
||||
mtx sync.RWMutex
|
||||
}
|
||||
|
||||
func newService(router adapter.ConnectionRouterEx, logger logger.ContextLogger, users []option.SSHUser) *service {
|
||||
return &service{
|
||||
router: router,
|
||||
logger: logger,
|
||||
users: users,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *service) PasswordCallback(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) {
|
||||
h.mtx.RLock()
|
||||
users := h.users
|
||||
h.mtx.RUnlock()
|
||||
for _, user := range users {
|
||||
if user.Name != "" && user.Name != conn.User() {
|
||||
continue
|
||||
}
|
||||
if user.Password != "" && user.Password == string(password) {
|
||||
return &ssh.Permissions{Extensions: map[string]string{"user": user.Name}}, nil
|
||||
}
|
||||
}
|
||||
return nil, E.New("password authentication failed for user ", conn.User())
|
||||
}
|
||||
|
||||
func (h *service) PublicKeyCallback(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
|
||||
h.mtx.RLock()
|
||||
users := h.users
|
||||
h.mtx.RUnlock()
|
||||
for _, user := range users {
|
||||
if user.Name != "" && user.Name != conn.User() {
|
||||
continue
|
||||
}
|
||||
for _, authorizedKey := range user.AuthorizedKeys {
|
||||
parsed, _, _, _, err := ssh.ParseAuthorizedKey([]byte(authorizedKey))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if bytes.Equal(parsed.Marshal(), key.Marshal()) {
|
||||
return &ssh.Permissions{Extensions: map[string]string{"user": user.Name}}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, E.New("public key authentication failed for user ", conn.User())
|
||||
}
|
||||
|
||||
func (h *service) UpdateUsers(users []option.SSHUser) {
|
||||
h.mtx.Lock()
|
||||
h.users = users
|
||||
h.mtx.Unlock()
|
||||
}
|
||||
|
||||
func (h *service) Handle(ctx context.Context, serverConn *ssh.ServerConn, channels <-chan ssh.NewChannel, requests <-chan *ssh.Request, metadata adapter.InboundContext, user string) {
|
||||
h.logger.InfoContext(ctx, "[", user, "] authenticated SSH connection from ", metadata.Source)
|
||||
go ssh.DiscardRequests(requests)
|
||||
for newChannel := range channels {
|
||||
switch newChannel.ChannelType() {
|
||||
case "direct-tcpip":
|
||||
go h.handleDirectChannel(ctx, metadata, newChannel)
|
||||
default:
|
||||
newChannel.Reject(ssh.UnknownChannelType, "only direct-tcpip is supported")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *service) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *service) handleDirectChannel(ctx context.Context, metadata adapter.InboundContext, newChannel ssh.NewChannel) {
|
||||
var payload directTCPIPData
|
||||
if err := ssh.Unmarshal(newChannel.ExtraData(), &payload); err != nil {
|
||||
newChannel.Reject(ssh.ConnectionFailed, "invalid direct-tcpip payload")
|
||||
h.logger.ErrorContext(ctx, E.Cause(err, "parse direct-tcpip payload"))
|
||||
return
|
||||
}
|
||||
channel, requests, err := newChannel.Accept()
|
||||
if err != nil {
|
||||
h.logger.ErrorContext(ctx, E.Cause(err, "accept direct-tcpip channel"))
|
||||
return
|
||||
}
|
||||
go ssh.DiscardRequests(requests)
|
||||
connMetadata := metadata
|
||||
connMetadata.Destination = M.ParseSocksaddrHostPort(payload.HostToConnect, uint16(payload.PortToConnect))
|
||||
conn := deadline.NewConn(&channelConn{
|
||||
Channel: channel,
|
||||
localAddr: metadata.OriginDestination.TCPAddr(),
|
||||
remoteAddr: metadata.Source.TCPAddr(),
|
||||
})
|
||||
h.logger.InfoContext(ctx, "[", metadata.User, "] inbound connection to ", connMetadata.Destination)
|
||||
h.router.RouteConnectionEx(ctx, conn, connMetadata, N.OnceClose(func(it error) {
|
||||
channel.Close()
|
||||
}))
|
||||
}
|
||||
|
||||
type directTCPIPData struct {
|
||||
HostToConnect string
|
||||
PortToConnect uint32
|
||||
OriginatorAddress string
|
||||
OriginatorPort uint32
|
||||
}
|
||||
|
||||
type channelConn struct {
|
||||
ssh.Channel
|
||||
localAddr net.Addr
|
||||
remoteAddr net.Addr
|
||||
}
|
||||
|
||||
func (c *channelConn) LocalAddr() net.Addr {
|
||||
return c.localAddr
|
||||
}
|
||||
|
||||
func (c *channelConn) RemoteAddr() net.Addr {
|
||||
return c.remoteAddr
|
||||
}
|
||||
|
||||
func (c *channelConn) SetDeadline(t time.Time) error {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
func (c *channelConn) SetReadDeadline(t time.Time) error {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
func (c *channelConn) SetWriteDeadline(t time.Time) error {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
@@ -151,10 +151,10 @@ func (t *DNSTransport) updateDNSServers(routeConfig *router.Config, dnsConfig *n
|
||||
}
|
||||
|
||||
if len(defaultResolvers) > 0 {
|
||||
t.logger.Info("updated ", len(routes), " routes, ", len(hosts), " hosts, default resolvers: ",
|
||||
t.logger.Notice("updated ", len(routes), " routes, ", len(hosts), " hosts, default resolvers: ",
|
||||
strings.Join(common.Map(dnsConfig.DefaultResolvers, func(it *dnstype.Resolver) string { return it.Addr }), " "))
|
||||
} else {
|
||||
t.logger.Info("updated ", len(routes), " routes, ", len(hosts), " hosts")
|
||||
t.logger.Notice("updated ", len(routes), " routes, ", len(hosts), " hosts")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -435,7 +435,7 @@ func (t *Endpoint) watchState() {
|
||||
}
|
||||
authURL := localBackend.StatusWithoutPeers().AuthURL
|
||||
if authURL != "" {
|
||||
t.logger.Info("Waiting for authentication: ", authURL)
|
||||
t.logger.Notice("Waiting for authentication: ", authURL)
|
||||
if t.platformInterface != nil {
|
||||
err := t.platformInterface.SendNotification(&adapter.Notification{
|
||||
Identifier: "tailscale-authentication",
|
||||
|
||||
@@ -388,7 +388,7 @@ func (t *Inbound) Start(stage adapter.StartStage) error {
|
||||
return err
|
||||
}
|
||||
t.tunStack = tunStack
|
||||
t.logger.Info("started at ", t.tunOptions.Name)
|
||||
t.logger.Notice("started at ", t.tunOptions.Name)
|
||||
case adapter.StartStatePostStart:
|
||||
monitor := taskmonitor.New(t.logger, C.StartTimeout)
|
||||
monitor.Start("starting tun stack")
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
stdtls "crypto/tls"
|
||||
"encoding/base64"
|
||||
"net"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@@ -96,7 +95,10 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
|
||||
return nil, E.New("unknown packet encoding: ", options.PacketEncoding)
|
||||
}
|
||||
}
|
||||
// Parse encryption configuration
|
||||
muxOpts := common.PtrValueOrDefault(options.Multiplex)
|
||||
if muxOpts.Enabled {
|
||||
options.Flow = ""
|
||||
}
|
||||
if options.Encryption != "" && options.Encryption != "none" {
|
||||
encryptionConfig, err := parseClientEncryption(options.Encryption)
|
||||
if err != nil {
|
||||
@@ -109,10 +111,6 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
|
||||
logger.Debug("encryption initialized: keys=", len(encryptionConfig.keys), " xorMode=", encryptionConfig.xorMode, " seconds=", encryptionConfig.seconds, " padding=", encryptionConfig.padding)
|
||||
}
|
||||
|
||||
muxOpts := common.PtrValueOrDefault(options.Multiplex)
|
||||
if muxOpts.Enabled {
|
||||
options.Flow = ""
|
||||
}
|
||||
outbound.client, err = vless.NewClient(options.UUID, options.Flow, logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -191,7 +189,6 @@ func (h *vlessDialer) DialContext(ctx context.Context, network string, destinati
|
||||
conn, err = h.transport.DialContext(ctx)
|
||||
if err == nil && h.vision {
|
||||
if baseConn == nil {
|
||||
// Only set baseConn if the transport delivered a TLS-capable connection
|
||||
if isVisionTLSConn(conn) {
|
||||
h.logger.Warn("Vision enabled but hook was not called by transport, using fallback")
|
||||
baseConn = conn
|
||||
@@ -210,7 +207,6 @@ func (h *vlessDialer) DialContext(ctx context.Context, network string, destinati
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Apply encryption if configured
|
||||
if h.encryption != nil {
|
||||
conn, err = h.encryption.Handshake(conn)
|
||||
if err != nil {
|
||||
@@ -218,36 +214,12 @@ func (h *vlessDialer) DialContext(ctx context.Context, network string, destinati
|
||||
}
|
||||
}
|
||||
|
||||
// For Vision: wrap the connection to expose the TLS/encryption connection for vless client
|
||||
var visionBaseConn net.Conn // The connection to pass to Vision (TLS or encryption layer)
|
||||
var visionBaseConn net.Conn
|
||||
var visionCanSplice bool
|
||||
if h.vision {
|
||||
isRAWTransport := h.transport == nil
|
||||
|
||||
if baseConn != nil && !isVisionTLSConn(baseConn) {
|
||||
baseConn = nil
|
||||
}
|
||||
if baseConn != nil {
|
||||
// Has TLS/Reality: use baseConn (TLS connection)
|
||||
visionBaseConn = baseConn
|
||||
visionCanSplice = isRAWTransport
|
||||
conn = newVisionConnWrapper(conn, baseConn)
|
||||
} else if h.encryption != nil {
|
||||
// Only has encryption (no TLS/Reality): use encryption layer itself
|
||||
encConn := findEncryptionLayer(conn)
|
||||
if encConn != nil {
|
||||
visionBaseConn = encConn
|
||||
if h.encryption.IsFullRandomXorMode() {
|
||||
visionCanSplice = false
|
||||
} else {
|
||||
visionCanSplice = isRAWTransport
|
||||
}
|
||||
conn = newVisionConnWrapper(conn, encConn)
|
||||
} else {
|
||||
return nil, E.New("Vision: failed to find encryption layer")
|
||||
}
|
||||
} else {
|
||||
return nil, E.New("Vision requires either TLS/Reality or Encryption")
|
||||
conn, visionBaseConn, visionCanSplice, err = h.setupVision(conn, baseConn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,8 +227,6 @@ func (h *vlessDialer) DialContext(ctx context.Context, network string, destinati
|
||||
case N.NetworkTCP:
|
||||
h.logger.InfoContext(ctx, "outbound connection to ", destination)
|
||||
if h.vision && visionBaseConn != nil {
|
||||
// For Vision, we need to pass the base connection (TLS or encryption layer)
|
||||
// to prepareConn so it can properly initialize VisionConn
|
||||
return h.client.DialEarlyConnWithOptions(conn, visionBaseConn, destination, visionCanSplice)
|
||||
}
|
||||
return h.client.DialEarlyConn(conn, destination)
|
||||
@@ -281,6 +251,29 @@ func (h *vlessDialer) DialContext(ctx context.Context, network string, destinati
|
||||
}
|
||||
}
|
||||
|
||||
func (h *vlessDialer) setupVision(conn net.Conn, baseConn net.Conn) (net.Conn, net.Conn, bool, error) {
|
||||
isRAWTransport := h.transport == nil
|
||||
|
||||
if baseConn != nil && !isVisionTLSConn(baseConn) {
|
||||
baseConn = nil
|
||||
}
|
||||
|
||||
if baseConn != nil {
|
||||
return newVisionConnWrapper(conn, baseConn), baseConn, isRAWTransport, nil
|
||||
}
|
||||
|
||||
if h.encryption != nil {
|
||||
encConn := findEncryptionLayer(conn)
|
||||
if encConn == nil {
|
||||
return nil, nil, false, E.New("Vision: failed to find encryption layer")
|
||||
}
|
||||
canSplice := isRAWTransport && !h.encryption.IsFullRandomXorMode()
|
||||
return newVisionConnWrapper(conn, encConn), encConn, canSplice, nil
|
||||
}
|
||||
|
||||
return nil, nil, false, E.New("Vision requires either TLS/Reality or Encryption")
|
||||
}
|
||||
|
||||
func (h *vlessDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||
h.logger.InfoContext(ctx, "outbound packet connection to ", destination)
|
||||
ctx, metadata := adapter.ExtendContext(ctx)
|
||||
@@ -299,7 +292,6 @@ func (h *vlessDialer) ListenPacket(ctx context.Context, destination M.Socksaddr)
|
||||
common.Close(conn)
|
||||
return nil, err
|
||||
}
|
||||
// Apply encryption if configured
|
||||
if h.encryption != nil {
|
||||
conn, err = h.encryption.Handshake(conn)
|
||||
if err != nil {
|
||||
@@ -362,7 +354,6 @@ func (c *visionConnWrapper) WriterReplaceable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// isVisionTLSConn returns true when the provided connection exposes TLS semantics Vision expects.
|
||||
func isVisionTLSConn(conn net.Conn) bool {
|
||||
if conn == nil {
|
||||
return false
|
||||
@@ -373,16 +364,6 @@ func isVisionTLSConn(conn net.Conn) bool {
|
||||
if _, ok := conn.(interface{ Handshake() error }); ok {
|
||||
return true
|
||||
}
|
||||
connType := reflect.TypeOf(conn)
|
||||
if connType == nil {
|
||||
return false
|
||||
}
|
||||
if connType.Kind() == reflect.Ptr {
|
||||
pkgPath := connType.Elem().PkgPath()
|
||||
if pkgPath == "crypto/tls" || strings.Contains(pkgPath, "utls") || strings.Contains(pkgPath, "shadowtls") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -218,7 +218,7 @@ func (s *ProviderRemote) fetch(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
s.logger.Info("update outbound provider ", s.Tag(), ": not modified")
|
||||
s.logger.Notice("update outbound provider ", s.Tag(), ": not modified")
|
||||
return nil
|
||||
default:
|
||||
return E.New("unexpected status: ", resp.Status)
|
||||
@@ -262,7 +262,7 @@ func (s *ProviderRemote) fetch(ctx context.Context) error {
|
||||
s.logger.Error("save outbound provider cache file: ", err)
|
||||
}
|
||||
}
|
||||
s.logger.Info("updated outbound provider ", s.Tag())
|
||||
s.logger.Notice("updated outbound provider ", s.Tag())
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -300,7 +300,7 @@ func (r *NetworkManager) UpdateInterfaces() error {
|
||||
oldInterface.Expensive == newInterface.Expensive &&
|
||||
oldInterface.Constrained == newInterface.Constrained
|
||||
}) {
|
||||
r.logger.Info("updated available networks: ", strings.Join(common.Map(newInterfaces, func(it adapter.NetworkInterface) string {
|
||||
r.logger.Notice("updated available networks: ", strings.Join(common.Map(newInterfaces, func(it adapter.NetworkInterface) string {
|
||||
var options []string
|
||||
options = append(options, F.ToString(it.Type))
|
||||
if it.Expensive {
|
||||
@@ -430,9 +430,9 @@ func (r *NetworkManager) onWIFIStateChanged(state adapter.WIFIState) {
|
||||
r.wifiState = state
|
||||
r.wifiStateMutex.Unlock()
|
||||
if state.SSID != "" {
|
||||
r.logger.Info("WIFI state changed: SSID=", state.SSID, ", BSSID=", state.BSSID)
|
||||
r.logger.Notice("WIFI state changed: SSID=", state.SSID, ", BSSID=", state.BSSID)
|
||||
} else {
|
||||
r.logger.Info("WIFI disconnected")
|
||||
r.logger.Notice("WIFI disconnected")
|
||||
}
|
||||
} else {
|
||||
r.wifiStateMutex.Unlock()
|
||||
@@ -512,7 +512,7 @@ func (r *NetworkManager) notifyInterfaceUpdate(defaultInterface *control.Interfa
|
||||
options = append(options, "constrained")
|
||||
}
|
||||
}
|
||||
r.logger.Info("updated default interface ", defaultInterface.Name, ", ", strings.Join(options, ", "))
|
||||
r.logger.Notice("updated default interface ", defaultInterface.Name, ", ", strings.Join(options, ", "))
|
||||
r.UpdateWIFIState()
|
||||
|
||||
if !r.started {
|
||||
@@ -538,5 +538,5 @@ func (r *NetworkManager) notifyWindowsPowerEvent(event int) {
|
||||
}
|
||||
|
||||
func (r *NetworkManager) OnPackagesUpdated(packages int, sharedUsers int) {
|
||||
r.logger.Info("updated packages list: ", packages, " packages, ", sharedUsers, " shared users")
|
||||
r.logger.Notice("updated packages list: ", packages, " packages, ", sharedUsers, " shared users")
|
||||
}
|
||||
|
||||
@@ -277,7 +277,7 @@ func (s *RemoteRuleSet) fetch(ctx context.Context, startContext *adapter.HTTPSta
|
||||
}
|
||||
}
|
||||
}
|
||||
s.logger.Info("update rule-set ", s.options.Tag, ": not modified")
|
||||
s.logger.Notice("update rule-set ", s.options.Tag, ": not modified")
|
||||
return nil
|
||||
default:
|
||||
return E.New("unexpected status: ", response.Status)
|
||||
@@ -308,7 +308,7 @@ func (s *RemoteRuleSet) fetch(ctx context.Context, startContext *adapter.HTTPSta
|
||||
s.logger.Error("save rule-set cache: ", err)
|
||||
}
|
||||
}
|
||||
s.logger.Info("updated rule-set ", s.options.Tag)
|
||||
s.logger.Notice("updated rule-set ", s.options.Tag)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ export type UserType =
|
||||
| "mtproxy"
|
||||
| "naive"
|
||||
| "socks"
|
||||
| "ssh"
|
||||
| "trojan"
|
||||
| "trusttunnel"
|
||||
| "tuic"
|
||||
@@ -57,6 +58,7 @@ export interface User {
|
||||
uuid: string;
|
||||
password: string;
|
||||
secret: string;
|
||||
authorized_keys: string[];
|
||||
flow: string;
|
||||
alter_id: number;
|
||||
created_at: string;
|
||||
@@ -70,6 +72,7 @@ export interface UserCreate {
|
||||
uuid?: string;
|
||||
password?: string;
|
||||
secret?: string;
|
||||
authorized_keys?: string[];
|
||||
flow?: string;
|
||||
alter_id?: number;
|
||||
}
|
||||
@@ -77,6 +80,7 @@ export interface UserUpdate {
|
||||
uuid?: string;
|
||||
password?: string;
|
||||
secret?: string;
|
||||
authorized_keys?: string[];
|
||||
flow?: string;
|
||||
alter_id?: number;
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ import type { Listable } from "../api/types";
|
||||
import { notifyApiError, useNotify } from "../notifications/NotificationsProvider";
|
||||
import { PageHeader } from "./PageHeader";
|
||||
|
||||
export type FieldType = "text" | "number" | "select" | "multiselect" | "ids" | "uuid";
|
||||
export type FieldType = "text" | "number" | "select" | "multiselect" | "ids" | "uuid" | "string-list";
|
||||
|
||||
// FILTER_WIDTH is the fixed CSS width (px) of a single filter cell in the
|
||||
// filter panel. FILTER_GAP is the flex gap between cells (matches the
|
||||
@@ -126,7 +126,7 @@ export interface FieldSpec<TValue = unknown> {
|
||||
// spec, matching what `emptyForm` would produce.
|
||||
function emptyValueForField(f: FieldSpec): unknown {
|
||||
if (f.defaultValue !== undefined) return f.defaultValue;
|
||||
if (f.type === "multiselect") return [];
|
||||
if (f.type === "multiselect" || f.type === "string-list") return [];
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -575,7 +575,7 @@ function fieldVisible(
|
||||
// strings, missing selections, and empty arrays for multi-select / ids.
|
||||
function isFieldEmpty(f: FieldSpec, value: unknown): boolean {
|
||||
if (value === undefined || value === null) return true;
|
||||
if (f.type === "multiselect" || f.type === "ids") {
|
||||
if (f.type === "multiselect" || f.type === "ids" || f.type === "string-list") {
|
||||
if (Array.isArray(value)) return value.length === 0;
|
||||
if (typeof value === "string") return value.trim() === "";
|
||||
return true;
|
||||
@@ -4764,6 +4764,40 @@ function CrudDialog({
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (f.type === "string-list") {
|
||||
const arr = Array.isArray(value) ? (value as string[]) : [];
|
||||
return (
|
||||
<Box key={f.name} sx={{ mt: -1 }}>
|
||||
<Typography variant="caption" color={errored ? "error" : "textSecondary"} sx={{ mb: 0.5, ml: "14px", display: "block" }}>
|
||||
{f.label}{f.required ? " *" : ""}
|
||||
</Typography>
|
||||
<Stack spacing={1}>
|
||||
{arr.map((item, idx) => (
|
||||
<Stack key={idx} direction="row" spacing={1} alignItems="center">
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
value={item}
|
||||
placeholder={`Key ${idx + 1}`}
|
||||
onChange={(e) => {
|
||||
const next = [...arr];
|
||||
next[idx] = e.target.value;
|
||||
set(f.name, next);
|
||||
}}
|
||||
/>
|
||||
<IconButton size="small" color="error" onClick={() => set(f.name, arr.filter((_, i) => i !== idx))}>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
<Button size="small" startIcon={<AddIcon />} sx={{ mt: 1 }} onClick={() => set(f.name, [...arr, ""])}>
|
||||
Add
|
||||
</Button>
|
||||
{fieldErr && <Typography variant="caption" color="error" sx={{ mt: 0.5, display: "block" }}>{fieldErr}</Typography>}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
const isNumber = f.type === "number";
|
||||
// Numeric value of the current cell. Falls back to 0 for the
|
||||
// empty state so the up-arrow always has a sensible base to
|
||||
|
||||
@@ -25,6 +25,7 @@ const USER_TYPES: { value: UserType; label: string }[] = [
|
||||
{ value: "mtproxy", label: "MTProxy" },
|
||||
{ value: "naive", label: "Naive" },
|
||||
{ value: "socks", label: "SOCKS" },
|
||||
{ value: "ssh", label: "SSH" },
|
||||
{ value: "trojan", label: "Trojan" },
|
||||
{ value: "trusttunnel", label: "TrustTunnel" },
|
||||
{ value: "tuic", label: "TUIC" },
|
||||
@@ -44,8 +45,9 @@ const FLOW_OPTIONS: { value: string; label: string }[] = [
|
||||
// same rule up-front (required fields invisible for the current type
|
||||
// are filtered out before validateRequired runs).
|
||||
const SHOW_UUID = new Set<UserType>(["vless", "vmess", "tuic"]);
|
||||
const SHOW_PASSWORD = new Set<UserType>(["anytls", "http", "hysteria", "hysteria2", "mixed", "naive", "socks", "trojan", "trusttunnel", "tuic"]);
|
||||
const SHOW_PASSWORD = new Set<UserType>(["anytls", "http", "hysteria", "hysteria2", "mixed", "naive", "socks", "ssh", "trojan", "trusttunnel", "tuic"]);
|
||||
const SHOW_SECRET = new Set<UserType>(["mtproxy"]);
|
||||
const SHOW_AUTHORIZED_KEYS = new Set<UserType>(["ssh"]);
|
||||
const SHOW_FLOW = new Set<UserType>(["vless"]);
|
||||
const SHOW_ALTER_ID = new Set<UserType>(["vmess"]);
|
||||
|
||||
@@ -103,7 +105,7 @@ export function UsersPage() {
|
||||
options: USER_TYPES,
|
||||
// Switching the user type wipes every credential field so the form
|
||||
// matches the legacy admin's behaviour of starting fresh.
|
||||
clears: ["uuid", "password", "secret", "flow", "alter_id"],
|
||||
clears: ["uuid", "password", "secret", "authorized_keys", "flow", "alter_id"],
|
||||
},
|
||||
// Credential fields: the Go struct validator reports "required" for
|
||||
// whichever of these is missing once the type is chosen, so each one
|
||||
@@ -111,8 +113,9 @@ export function UsersPage() {
|
||||
// are filtered out before validateRequired runs, so e.g. Password is
|
||||
// only enforced for hysteria/hysteria2/trojan/tuic and not for vless.
|
||||
{ name: "uuid", label: "UUID", type: "uuid", required: true, visibleWhen: showFor(SHOW_UUID) },
|
||||
{ name: "password", label: "Password", type: "text", required: true, visibleWhen: showFor(SHOW_PASSWORD) },
|
||||
{ name: "password", label: "Password", type: "text", visibleWhen: showFor(SHOW_PASSWORD) },
|
||||
{ name: "secret", label: "Secret", type: "text", required: true, visibleWhen: showFor(SHOW_SECRET) },
|
||||
{ name: "authorized_keys", label: "Authorized Keys", type: "string-list", visibleWhen: showFor(SHOW_AUTHORIZED_KEYS) },
|
||||
{
|
||||
name: "flow",
|
||||
label: "Flow",
|
||||
@@ -135,6 +138,7 @@ export function UsersPage() {
|
||||
uuid: u.uuid,
|
||||
password: u.password,
|
||||
secret: u.secret,
|
||||
authorized_keys: u.authorized_keys ?? [],
|
||||
flow: u.flow,
|
||||
alter_id: u.alter_id,
|
||||
}),
|
||||
@@ -146,6 +150,7 @@ export function UsersPage() {
|
||||
uuid: f.uuid ? String(f.uuid).trim() : undefined,
|
||||
password: f.password ? String(f.password) : undefined,
|
||||
secret: f.secret ? String(f.secret) : undefined,
|
||||
authorized_keys: Array.isArray(f.authorized_keys) ? (f.authorized_keys as string[]).filter(Boolean) : undefined,
|
||||
flow: f.flow ? String(f.flow) : undefined,
|
||||
alter_id: f.alter_id !== undefined && f.alter_id !== "" ? Number(f.alter_id) : undefined,
|
||||
}),
|
||||
@@ -154,6 +159,7 @@ export function UsersPage() {
|
||||
if (f.uuid && String(f.uuid).trim() !== "") out.uuid = String(f.uuid).trim();
|
||||
if (f.password !== undefined && f.password !== "") out.password = String(f.password);
|
||||
if (f.secret !== undefined && f.secret !== "") out.secret = String(f.secret);
|
||||
if (Array.isArray(f.authorized_keys) && (f.authorized_keys as string[]).filter(Boolean).length > 0) out.authorized_keys = (f.authorized_keys as string[]).filter(Boolean);
|
||||
if (f.flow !== undefined && f.flow !== "") out.flow = String(f.flow);
|
||||
if (f.alter_id !== undefined && f.alter_id !== "") out.alter_id = Number(f.alter_id);
|
||||
return out;
|
||||
|
||||
@@ -49,46 +49,50 @@ type BaseNode struct {
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID int `json:"id" validate:"required"`
|
||||
SquadIDs []int `json:"squad_ids" validate:"required,min=1"`
|
||||
Username string `json:"username" validate:"required"`
|
||||
Inbound string `json:"inbound" validate:"required"`
|
||||
Type string `json:"type" validate:"required"`
|
||||
UUID string `json:"uuid" validate:"required"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
Secret string `json:"secret" validate:"required"`
|
||||
Flow string `json:"flow" validate:"required"`
|
||||
AlterID int `json:"alter_id" validate:"required"`
|
||||
CreatedAt time.Time `json:"created_at" validate:"required"`
|
||||
UpdatedAt time.Time `json:"updated_at" validate:"required"`
|
||||
ID int `json:"id" validate:"required"`
|
||||
SquadIDs []int `json:"squad_ids" validate:"required,min=1"`
|
||||
Username string `json:"username" validate:"required"`
|
||||
Inbound string `json:"inbound" validate:"required"`
|
||||
Type string `json:"type" validate:"required"`
|
||||
UUID string `json:"uuid" validate:"required"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
Secret string `json:"secret" validate:"required"`
|
||||
AuthorizedKeys []string `json:"authorized_keys" validate:"omitempty"`
|
||||
Flow string `json:"flow" validate:"required"`
|
||||
AlterID int `json:"alter_id" validate:"required"`
|
||||
CreatedAt time.Time `json:"created_at" validate:"required"`
|
||||
UpdatedAt time.Time `json:"updated_at" validate:"required"`
|
||||
}
|
||||
|
||||
type UserCreate struct {
|
||||
SquadIDs []int `json:"squad_ids" validate:"required,min=1"`
|
||||
Username string `json:"username" validate:"required"`
|
||||
Inbound string `json:"inbound" validate:"required"`
|
||||
Type string `json:"type" validate:"required,oneof=anytls http hysteria hysteria2 mixed mtproxy naive socks trojan trusttunnel tuic vless vmess"`
|
||||
UUID string `json:"uuid" validate:"omitempty,uuid4"`
|
||||
Password string `json:"password" validate:"omitempty"`
|
||||
Secret string `json:"secret" validate:"omitempty"`
|
||||
Flow string `json:"flow" validate:"omitempty"`
|
||||
AlterID int `json:"alter_id" validate:"omitempty"`
|
||||
SquadIDs []int `json:"squad_ids" validate:"required,min=1"`
|
||||
Username string `json:"username" validate:"required"`
|
||||
Inbound string `json:"inbound" validate:"required"`
|
||||
Type string `json:"type" validate:"required,oneof=anytls http hysteria hysteria2 mixed mtproxy naive socks ssh trojan trusttunnel tuic vless vmess"`
|
||||
UUID string `json:"uuid" validate:"omitempty,uuid4"`
|
||||
Password string `json:"password" validate:"omitempty"`
|
||||
Secret string `json:"secret" validate:"omitempty"`
|
||||
AuthorizedKeys []string `json:"authorized_keys" validate:"omitempty"`
|
||||
Flow string `json:"flow" validate:"omitempty"`
|
||||
AlterID int `json:"alter_id" validate:"omitempty"`
|
||||
}
|
||||
|
||||
type UserUpdate struct {
|
||||
UUID string `json:"uuid" validate:"omitempty,uuid4"`
|
||||
Password string `json:"password" validate:"omitempty"`
|
||||
Secret string `json:"secret" validate:"omitempty"`
|
||||
Flow string `json:"flow" validate:"omitempty"`
|
||||
AlterID int `json:"alter_id" validate:"omitempty"`
|
||||
UUID string `json:"uuid" validate:"omitempty,uuid4"`
|
||||
Password string `json:"password" validate:"omitempty"`
|
||||
Secret string `json:"secret" validate:"omitempty"`
|
||||
AuthorizedKeys []string `json:"authorized_keys" validate:"omitempty"`
|
||||
Flow string `json:"flow" validate:"omitempty"`
|
||||
AlterID int `json:"alter_id" validate:"omitempty"`
|
||||
}
|
||||
|
||||
type BaseUser struct {
|
||||
UUID string `json:"uuid" validate:"omitempty,uuid4"`
|
||||
Password string `json:"password" validate:"omitempty"`
|
||||
Secret string `json:"secret" validate:"omitempty"`
|
||||
Flow string `json:"flow" validate:"omitempty"`
|
||||
AlterID int `json:"alter_id" validate:"omitempty"`
|
||||
UUID string `json:"uuid" validate:"omitempty,uuid4"`
|
||||
Password string `json:"password" validate:"omitempty"`
|
||||
Secret string `json:"secret" validate:"omitempty"`
|
||||
AuthorizedKeys []string `json:"authorized_keys" validate:"omitempty"`
|
||||
Flow string `json:"flow" validate:"omitempty"`
|
||||
AlterID int `json:"alter_id" validate:"omitempty"`
|
||||
}
|
||||
|
||||
type ConnectionLimiter struct {
|
||||
@@ -261,5 +265,3 @@ type BaseRateLimiter struct {
|
||||
Count uint32 `json:"count" validate:"excluded_if=Strategy bypass,required_unless=Strategy bypass"`
|
||||
Interval string `json:"interval" validate:"excluded_if=Strategy bypass,required_unless=Strategy bypass"`
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -51,5 +51,3 @@ type Repository interface {
|
||||
UpdateRateLimiter(id int, limiter RateLimiterUpdate) (RateLimiter, error)
|
||||
DeleteRateLimiter(id int) (RateLimiter, error)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -331,6 +331,12 @@ var migrations = map[string]string{
|
||||
DROP TABLE IF EXISTS traffic_limiter_to_squad;
|
||||
DROP TABLE IF EXISTS traffic_limiters;
|
||||
`,
|
||||
"3_add_authorized_keys.up.sql": `
|
||||
ALTER TABLE users ADD COLUMN authorized_keys JSONB NOT NULL DEFAULT '[]'::jsonb;
|
||||
`,
|
||||
"3_add_authorized_keys.down.sql": `
|
||||
ALTER TABLE users DROP COLUMN authorized_keys;
|
||||
`,
|
||||
}
|
||||
|
||||
func Migrate(db *sql.DB) error {
|
||||
|
||||
@@ -515,6 +515,11 @@ func (r *PostgreSQLRepository) CreateUser(user constant.UserCreate) (constant.Us
|
||||
}
|
||||
defer tx.Rollback(r.ctx)
|
||||
now := time.Now()
|
||||
authorizedKeysJSON, err := marshalStringSlice(user.AuthorizedKeys)
|
||||
if err != nil {
|
||||
return u, err
|
||||
}
|
||||
var authorizedKeys stringSliceJSON
|
||||
err = tx.QueryRow(
|
||||
r.ctx, `
|
||||
INSERT INTO users (
|
||||
@@ -524,12 +529,13 @@ func (r *PostgreSQLRepository) CreateUser(user constant.UserCreate) (constant.Us
|
||||
uuid,
|
||||
password,
|
||||
secret,
|
||||
authorized_keys,
|
||||
flow,
|
||||
alter_id,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||
RETURNING
|
||||
id,
|
||||
username,
|
||||
@@ -538,6 +544,7 @@ func (r *PostgreSQLRepository) CreateUser(user constant.UserCreate) (constant.Us
|
||||
uuid,
|
||||
password,
|
||||
secret,
|
||||
authorized_keys,
|
||||
flow,
|
||||
alter_id,
|
||||
created_at,
|
||||
@@ -549,6 +556,7 @@ func (r *PostgreSQLRepository) CreateUser(user constant.UserCreate) (constant.Us
|
||||
user.UUID,
|
||||
user.Password,
|
||||
user.Secret,
|
||||
authorizedKeysJSON,
|
||||
user.Flow,
|
||||
user.AlterID,
|
||||
now,
|
||||
@@ -561,6 +569,7 @@ func (r *PostgreSQLRepository) CreateUser(user constant.UserCreate) (constant.Us
|
||||
&u.UUID,
|
||||
&u.Password,
|
||||
&u.Secret,
|
||||
&authorizedKeys,
|
||||
&u.Flow,
|
||||
&u.AlterID,
|
||||
&u.CreatedAt,
|
||||
@@ -569,6 +578,7 @@ func (r *PostgreSQLRepository) CreateUser(user constant.UserCreate) (constant.Us
|
||||
if err != nil {
|
||||
return u, err
|
||||
}
|
||||
u.AuthorizedKeys = []string(authorizedKeys)
|
||||
rows := make([][]any, len(user.SquadIDs))
|
||||
for i, squadID := range user.SquadIDs {
|
||||
rows[i] = []any{u.ID, squadID}
|
||||
@@ -605,6 +615,7 @@ func (r *PostgreSQLRepository) GetUsers(filters map[string][]string) ([]constant
|
||||
"uuid",
|
||||
"password",
|
||||
"secret",
|
||||
"authorized_keys",
|
||||
"flow",
|
||||
"alter_id",
|
||||
"created_at",
|
||||
@@ -636,6 +647,7 @@ func (r *PostgreSQLRepository) GetUsers(filters map[string][]string) ([]constant
|
||||
&u.UUID,
|
||||
&u.Password,
|
||||
&u.Secret,
|
||||
&u.AuthorizedKeys,
|
||||
&u.Flow,
|
||||
&u.AlterID,
|
||||
&u.CreatedAt,
|
||||
@@ -681,6 +693,7 @@ func (r *PostgreSQLRepository) GetUser(id int) (constant.User, error) {
|
||||
uuid,
|
||||
password,
|
||||
secret,
|
||||
authorized_keys,
|
||||
flow,
|
||||
alter_id,
|
||||
created_at,
|
||||
@@ -696,6 +709,7 @@ func (r *PostgreSQLRepository) GetUser(id int) (constant.User, error) {
|
||||
&u.UUID,
|
||||
&u.Password,
|
||||
&u.Secret,
|
||||
&u.AuthorizedKeys,
|
||||
&u.Flow,
|
||||
&u.AlterID,
|
||||
&u.CreatedAt,
|
||||
@@ -706,17 +720,22 @@ func (r *PostgreSQLRepository) GetUser(id int) (constant.User, error) {
|
||||
|
||||
func (r *PostgreSQLRepository) UpdateUser(id int, user constant.UserUpdate) (constant.User, error) {
|
||||
var u constant.User
|
||||
err := r.db.QueryRow(
|
||||
authorizedKeysJSON, err := marshalStringSlice(user.AuthorizedKeys)
|
||||
if err != nil {
|
||||
return u, err
|
||||
}
|
||||
err = r.db.QueryRow(
|
||||
r.ctx, `
|
||||
UPDATE users
|
||||
SET
|
||||
uuid = $1,
|
||||
password = $2,
|
||||
secret = $3,
|
||||
flow = $4,
|
||||
alter_id = $5,
|
||||
updated_at = $6
|
||||
WHERE id = $7
|
||||
authorized_keys = $4,
|
||||
flow = $5,
|
||||
alter_id = $6,
|
||||
updated_at = $7
|
||||
WHERE id = $8
|
||||
RETURNING
|
||||
id,
|
||||
ARRAY(
|
||||
@@ -730,6 +749,7 @@ func (r *PostgreSQLRepository) UpdateUser(id int, user constant.UserUpdate) (con
|
||||
uuid,
|
||||
password,
|
||||
secret,
|
||||
authorized_keys,
|
||||
flow,
|
||||
alter_id,
|
||||
created_at,
|
||||
@@ -738,6 +758,7 @@ func (r *PostgreSQLRepository) UpdateUser(id int, user constant.UserUpdate) (con
|
||||
user.UUID,
|
||||
user.Password,
|
||||
user.Secret,
|
||||
authorizedKeysJSON,
|
||||
user.Flow,
|
||||
user.AlterID,
|
||||
time.Now(),
|
||||
@@ -751,6 +772,7 @@ func (r *PostgreSQLRepository) UpdateUser(id int, user constant.UserUpdate) (con
|
||||
&u.UUID,
|
||||
&u.Password,
|
||||
&u.Secret,
|
||||
&u.AuthorizedKeys,
|
||||
&u.Flow,
|
||||
&u.AlterID,
|
||||
&u.CreatedAt,
|
||||
@@ -777,6 +799,7 @@ func (r *PostgreSQLRepository) DeleteUser(id int) (constant.User, error) {
|
||||
uuid,
|
||||
password,
|
||||
secret,
|
||||
authorized_keys,
|
||||
flow,
|
||||
alter_id,
|
||||
created_at,
|
||||
@@ -790,6 +813,7 @@ func (r *PostgreSQLRepository) DeleteUser(id int) (constant.User, error) {
|
||||
&u.UUID,
|
||||
&u.Password,
|
||||
&u.Secret,
|
||||
&u.AuthorizedKeys,
|
||||
&u.Flow,
|
||||
&u.AlterID,
|
||||
&u.CreatedAt,
|
||||
@@ -2143,11 +2167,11 @@ func init() {
|
||||
"updated_at_end": LessThanFilter("updated_at"),
|
||||
"sort_asc": ReplacedSortAscFilter(
|
||||
map[string]string{"speed": "raw_speed"},
|
||||
[]string{"id", "username", "outbound", "strategy", "mode", "raw_speed", "created_at", "updated_at"},
|
||||
[]string{"id", "username", "outbound", "strategy", "connection_type", "mode", "raw_speed", "created_at", "updated_at"},
|
||||
),
|
||||
"sort_desc": ReplacedSortDescFilter(
|
||||
map[string]string{"speed": "raw_speed"},
|
||||
[]string{"id", "username", "outbound", "strategy", "mode", "raw_speed", "created_at", "updated_at"},
|
||||
[]string{"id", "username", "outbound", "strategy", "connection_type", "mode", "raw_speed", "created_at", "updated_at"},
|
||||
),
|
||||
"offset": OffsetFilter(),
|
||||
"limit": LimitFilter(),
|
||||
|
||||
@@ -213,6 +213,12 @@ var migrations = map[string]string{
|
||||
DROP TABLE IF EXISTS nodes;
|
||||
DROP TABLE IF EXISTS squads;
|
||||
`,
|
||||
"2_add_authorized_keys.up.sql": `
|
||||
ALTER TABLE users ADD COLUMN authorized_keys TEXT NOT NULL DEFAULT '[]';
|
||||
`,
|
||||
"2_add_authorized_keys.down.sql": `
|
||||
ALTER TABLE users DROP COLUMN authorized_keys;
|
||||
`,
|
||||
}
|
||||
|
||||
func Migrate(db *sql.DB) error {
|
||||
|
||||
@@ -510,6 +510,11 @@ func (r *SQLiteRepository) CreateUser(user constant.UserCreate) (constant.User,
|
||||
}
|
||||
defer tx.Rollback()
|
||||
now := time.Now()
|
||||
authorizedKeysJSON, err := marshalStringSlice(user.AuthorizedKeys)
|
||||
if err != nil {
|
||||
return u, err
|
||||
}
|
||||
var authorizedKeys stringSliceJSON
|
||||
err = tx.QueryRowContext(
|
||||
r.ctx, `
|
||||
INSERT INTO users (
|
||||
@@ -519,12 +524,13 @@ func (r *SQLiteRepository) CreateUser(user constant.UserCreate) (constant.User,
|
||||
uuid,
|
||||
password,
|
||||
secret,
|
||||
authorized_keys,
|
||||
flow,
|
||||
alter_id,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
RETURNING
|
||||
id,
|
||||
username,
|
||||
@@ -533,6 +539,7 @@ func (r *SQLiteRepository) CreateUser(user constant.UserCreate) (constant.User,
|
||||
uuid,
|
||||
password,
|
||||
secret,
|
||||
authorized_keys,
|
||||
flow,
|
||||
alter_id,
|
||||
created_at,
|
||||
@@ -544,6 +551,7 @@ func (r *SQLiteRepository) CreateUser(user constant.UserCreate) (constant.User,
|
||||
user.UUID,
|
||||
user.Password,
|
||||
user.Secret,
|
||||
authorizedKeysJSON,
|
||||
user.Flow,
|
||||
user.AlterID,
|
||||
now,
|
||||
@@ -556,6 +564,7 @@ func (r *SQLiteRepository) CreateUser(user constant.UserCreate) (constant.User,
|
||||
&u.UUID,
|
||||
&u.Password,
|
||||
&u.Secret,
|
||||
&authorizedKeys,
|
||||
&u.Flow,
|
||||
&u.AlterID,
|
||||
&u.CreatedAt,
|
||||
@@ -564,6 +573,7 @@ func (r *SQLiteRepository) CreateUser(user constant.UserCreate) (constant.User,
|
||||
if err != nil {
|
||||
return u, err
|
||||
}
|
||||
u.AuthorizedKeys = []string(authorizedKeys)
|
||||
stmt, err := tx.PrepareContext(r.ctx, `INSERT INTO user_to_squad (user_id, squad_id) VALUES (?, ?)`)
|
||||
if err != nil {
|
||||
return u, err
|
||||
@@ -596,6 +606,7 @@ func (r *SQLiteRepository) GetUsers(filters map[string][]string) ([]constant.Use
|
||||
"uuid",
|
||||
"password",
|
||||
"secret",
|
||||
"authorized_keys",
|
||||
"flow",
|
||||
"alter_id",
|
||||
"created_at",
|
||||
@@ -619,6 +630,7 @@ func (r *SQLiteRepository) GetUsers(filters map[string][]string) ([]constant.Use
|
||||
for rows.Next() {
|
||||
var u constant.User
|
||||
var squadIDs intSliceJSON
|
||||
var authorizedKeys stringSliceJSON
|
||||
if err := rows.Scan(
|
||||
&u.ID,
|
||||
&squadIDs,
|
||||
@@ -628,6 +640,7 @@ func (r *SQLiteRepository) GetUsers(filters map[string][]string) ([]constant.Use
|
||||
&u.UUID,
|
||||
&u.Password,
|
||||
&u.Secret,
|
||||
&authorizedKeys,
|
||||
&u.Flow,
|
||||
&u.AlterID,
|
||||
&u.CreatedAt,
|
||||
@@ -636,6 +649,7 @@ func (r *SQLiteRepository) GetUsers(filters map[string][]string) ([]constant.Use
|
||||
return nil, err
|
||||
}
|
||||
u.SquadIDs = []int(squadIDs)
|
||||
u.AuthorizedKeys = []string(authorizedKeys)
|
||||
result = append(result, u)
|
||||
}
|
||||
return result, rows.Err()
|
||||
@@ -661,6 +675,7 @@ func (r *SQLiteRepository) GetUsersCount(filters map[string][]string) (int, erro
|
||||
func (r *SQLiteRepository) GetUser(id int) (constant.User, error) {
|
||||
var u constant.User
|
||||
var squadIDs intSliceJSON
|
||||
var authorizedKeys stringSliceJSON
|
||||
err := r.db.QueryRowContext(r.ctx, `
|
||||
SELECT
|
||||
id,
|
||||
@@ -675,6 +690,7 @@ func (r *SQLiteRepository) GetUser(id int) (constant.User, error) {
|
||||
uuid,
|
||||
password,
|
||||
secret,
|
||||
authorized_keys,
|
||||
flow,
|
||||
alter_id,
|
||||
created_at,
|
||||
@@ -690,25 +706,33 @@ func (r *SQLiteRepository) GetUser(id int) (constant.User, error) {
|
||||
&u.UUID,
|
||||
&u.Password,
|
||||
&u.Secret,
|
||||
&authorizedKeys,
|
||||
&u.Flow,
|
||||
&u.AlterID,
|
||||
&u.CreatedAt,
|
||||
&u.UpdatedAt,
|
||||
)
|
||||
u.SquadIDs = []int(squadIDs)
|
||||
u.AuthorizedKeys = []string(authorizedKeys)
|
||||
return u, notFoundErr(err)
|
||||
}
|
||||
|
||||
func (r *SQLiteRepository) UpdateUser(id int, user constant.UserUpdate) (constant.User, error) {
|
||||
var u constant.User
|
||||
var squadIDs intSliceJSON
|
||||
err := r.db.QueryRowContext(
|
||||
var authorizedKeys stringSliceJSON
|
||||
authorizedKeysJSON, err := marshalStringSlice(user.AuthorizedKeys)
|
||||
if err != nil {
|
||||
return u, err
|
||||
}
|
||||
err = r.db.QueryRowContext(
|
||||
r.ctx, `
|
||||
UPDATE users
|
||||
SET
|
||||
uuid = ?,
|
||||
password = ?,
|
||||
secret = ?,
|
||||
authorized_keys = ?,
|
||||
flow = ?,
|
||||
alter_id = ?,
|
||||
updated_at = ?
|
||||
@@ -726,6 +750,7 @@ func (r *SQLiteRepository) UpdateUser(id int, user constant.UserUpdate) (constan
|
||||
uuid,
|
||||
password,
|
||||
secret,
|
||||
authorized_keys,
|
||||
flow,
|
||||
alter_id,
|
||||
created_at,
|
||||
@@ -734,6 +759,7 @@ func (r *SQLiteRepository) UpdateUser(id int, user constant.UserUpdate) (constan
|
||||
user.UUID,
|
||||
user.Password,
|
||||
user.Secret,
|
||||
authorizedKeysJSON,
|
||||
user.Flow,
|
||||
user.AlterID,
|
||||
time.Now(),
|
||||
@@ -747,18 +773,21 @@ func (r *SQLiteRepository) UpdateUser(id int, user constant.UserUpdate) (constan
|
||||
&u.UUID,
|
||||
&u.Password,
|
||||
&u.Secret,
|
||||
&authorizedKeys,
|
||||
&u.Flow,
|
||||
&u.AlterID,
|
||||
&u.CreatedAt,
|
||||
&u.UpdatedAt,
|
||||
)
|
||||
u.SquadIDs = []int(squadIDs)
|
||||
u.AuthorizedKeys = []string(authorizedKeys)
|
||||
return u, err
|
||||
}
|
||||
|
||||
func (r *SQLiteRepository) DeleteUser(id int) (constant.User, error) {
|
||||
var u constant.User
|
||||
var squadIDs intSliceJSON
|
||||
var authorizedKeys stringSliceJSON
|
||||
err := r.db.QueryRowContext(r.ctx, `
|
||||
DELETE FROM users
|
||||
WHERE id = ?
|
||||
@@ -775,6 +804,7 @@ func (r *SQLiteRepository) DeleteUser(id int) (constant.User, error) {
|
||||
uuid,
|
||||
password,
|
||||
secret,
|
||||
authorized_keys,
|
||||
flow,
|
||||
alter_id,
|
||||
created_at,
|
||||
@@ -788,12 +818,14 @@ func (r *SQLiteRepository) DeleteUser(id int) (constant.User, error) {
|
||||
&u.UUID,
|
||||
&u.Password,
|
||||
&u.Secret,
|
||||
&authorizedKeys,
|
||||
&u.Flow,
|
||||
&u.AlterID,
|
||||
&u.CreatedAt,
|
||||
&u.UpdatedAt,
|
||||
)
|
||||
u.SquadIDs = []int(squadIDs)
|
||||
u.AuthorizedKeys = []string(authorizedKeys)
|
||||
return u, err
|
||||
}
|
||||
|
||||
@@ -2160,11 +2192,11 @@ func init() {
|
||||
"updated_at_end": LessThanFilter("updated_at"),
|
||||
"sort_asc": ReplacedSortAscFilter(
|
||||
map[string]string{"speed": "raw_speed"},
|
||||
[]string{"id", "username", "outbound", "strategy", "mode", "raw_speed", "created_at", "updated_at"},
|
||||
[]string{"id", "username", "outbound", "strategy", "connection_type", "mode", "raw_speed", "created_at", "updated_at"},
|
||||
),
|
||||
"sort_desc": ReplacedSortDescFilter(
|
||||
map[string]string{"speed": "raw_speed"},
|
||||
[]string{"id", "username", "outbound", "strategy", "mode", "raw_speed", "created_at", "updated_at"},
|
||||
[]string{"id", "username", "outbound", "strategy", "connection_type", "mode", "raw_speed", "created_at", "updated_at"},
|
||||
),
|
||||
"offset": OffsetFilter(),
|
||||
"limit": LimitFilter(),
|
||||
|
||||
@@ -32,10 +32,10 @@ func RegisterService(registry *boxService.Registry) {
|
||||
|
||||
type Service struct {
|
||||
boxService.Adapter
|
||||
ctx context.Context
|
||||
logger log.ContextLogger
|
||||
repository constant.Repository
|
||||
nodes map[string]constant.ConnectedNode
|
||||
ctx context.Context
|
||||
logger log.ContextLogger
|
||||
repository constant.Repository
|
||||
nodes map[string]constant.ConnectedNode
|
||||
|
||||
limiterLocks map[int]map[string]*cache.Cache[string, struct{}]
|
||||
trafficUsage map[int]*TrafficUsage
|
||||
@@ -93,6 +93,10 @@ func NewService(ctx context.Context, logger log.ContextLogger, tag string, optio
|
||||
if user.Password == "" {
|
||||
sl.ReportError(user.Password, "password", "Password", "required", "")
|
||||
}
|
||||
case "ssh":
|
||||
if user.Password == "" && len(user.AuthorizedKeys) == 0 {
|
||||
sl.ReportError(user.Password, "password", "Password", "required_without", "")
|
||||
}
|
||||
case "mtproxy":
|
||||
if user.Secret == "" {
|
||||
sl.ReportError(user.Secret, "secret", "Secret", "required", "")
|
||||
|
||||
@@ -59,18 +59,19 @@ func convertNode(v *pb.Node) CM.Node {
|
||||
|
||||
func convertUser(v *pb.User) CM.User {
|
||||
return CM.User{
|
||||
ID: int(v.GetId()),
|
||||
SquadIDs: toIntSlice(v.GetSquadIds()),
|
||||
Username: v.GetUsername(),
|
||||
Inbound: v.GetInbound(),
|
||||
Type: v.GetType(),
|
||||
UUID: v.GetUuid(),
|
||||
Password: v.GetPassword(),
|
||||
Secret: v.GetSecret(),
|
||||
Flow: v.GetFlow(),
|
||||
AlterID: int(v.GetAlterId()),
|
||||
CreatedAt: timeFromNano(v.GetCreatedAt()),
|
||||
UpdatedAt: timeFromNano(v.GetUpdatedAt()),
|
||||
ID: int(v.GetId()),
|
||||
SquadIDs: toIntSlice(v.GetSquadIds()),
|
||||
Username: v.GetUsername(),
|
||||
Inbound: v.GetInbound(),
|
||||
Type: v.GetType(),
|
||||
UUID: v.GetUuid(),
|
||||
Password: v.GetPassword(),
|
||||
Secret: v.GetSecret(),
|
||||
AuthorizedKeys: v.GetAuthorizedKeys(),
|
||||
Flow: v.GetFlow(),
|
||||
AlterID: int(v.GetAlterId()),
|
||||
CreatedAt: timeFromNano(v.GetCreatedAt()),
|
||||
UpdatedAt: timeFromNano(v.GetUpdatedAt()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +84,7 @@ func convertBandwidthLimiter(v *pb.BandwidthLimiter) CM.BandwidthLimiter {
|
||||
Strategy: v.GetStrategy(),
|
||||
ConnectionType: v.GetConnectionType(),
|
||||
Mode: v.GetMode(),
|
||||
FlowKeys: v.GetFlowKeys(),
|
||||
FlowKeys: v.GetFlowKeys(),
|
||||
Speed: v.GetSpeed(),
|
||||
RawSpeed: v.GetRawSpeed(),
|
||||
CreatedAt: timeFromNano(v.GetCreatedAt()),
|
||||
|
||||
@@ -188,15 +188,16 @@ func (s *Client) CreateUser(in CM.UserCreate) (CM.User, error) {
|
||||
return CM.User{}, err
|
||||
}
|
||||
reply, err := c.CreateUser(s.callContext(), &pb.UserCreate{
|
||||
SquadIds: toInt32Slice(in.SquadIDs),
|
||||
Username: in.Username,
|
||||
Inbound: in.Inbound,
|
||||
Type: in.Type,
|
||||
Uuid: in.UUID,
|
||||
Password: in.Password,
|
||||
Secret: in.Secret,
|
||||
Flow: in.Flow,
|
||||
AlterId: int32(in.AlterID),
|
||||
SquadIds: toInt32Slice(in.SquadIDs),
|
||||
Username: in.Username,
|
||||
Inbound: in.Inbound,
|
||||
Type: in.Type,
|
||||
Uuid: in.UUID,
|
||||
Password: in.Password,
|
||||
Secret: in.Secret,
|
||||
AuthorizedKeys: in.AuthorizedKeys,
|
||||
Flow: in.Flow,
|
||||
AlterId: int32(in.AlterID),
|
||||
})
|
||||
if err != nil {
|
||||
return CM.User{}, mapError(err)
|
||||
@@ -252,11 +253,12 @@ func (s *Client) UpdateUser(id int, in CM.UserUpdate) (CM.User, error) {
|
||||
reply, err := c.UpdateUser(s.callContext(), &pb.UserUpdateRequest{
|
||||
Id: int32(id),
|
||||
Update: &pb.UserUpdate{
|
||||
Uuid: in.UUID,
|
||||
Password: in.Password,
|
||||
Secret: in.Secret,
|
||||
Flow: in.Flow,
|
||||
AlterId: int32(in.AlterID),
|
||||
Uuid: in.UUID,
|
||||
Password: in.Password,
|
||||
Secret: in.Secret,
|
||||
AuthorizedKeys: in.AuthorizedKeys,
|
||||
Flow: in.Flow,
|
||||
AlterId: int32(in.AlterID),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
@@ -289,7 +291,7 @@ func (s *Client) CreateBandwidthLimiter(in CM.BandwidthLimiterCreate) (CM.Bandwi
|
||||
Strategy: in.Strategy,
|
||||
ConnectionType: in.ConnectionType,
|
||||
Mode: in.Mode,
|
||||
FlowKeys: in.FlowKeys,
|
||||
FlowKeys: in.FlowKeys,
|
||||
Speed: in.Speed,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -351,7 +353,7 @@ func (s *Client) UpdateBandwidthLimiter(id int, in CM.BandwidthLimiterUpdate) (C
|
||||
Strategy: in.Strategy,
|
||||
ConnectionType: in.ConnectionType,
|
||||
Mode: in.Mode,
|
||||
FlowKeys: in.FlowKeys,
|
||||
FlowKeys: in.FlowKeys,
|
||||
Speed: in.Speed,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v6.31.1
|
||||
// protoc v7.34.1
|
||||
// source: service/manager_api/grpc/manager/manager.proto
|
||||
|
||||
package manager
|
||||
@@ -550,21 +550,22 @@ func (x *NodeUpdateRequest) GetUpdate() *NodeUpdate {
|
||||
}
|
||||
|
||||
type User struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
SquadIds []int32 `protobuf:"varint,2,rep,packed,name=squad_ids,json=squadIds,proto3" json:"squad_ids,omitempty"`
|
||||
Username string `protobuf:"bytes,3,opt,name=username,proto3" json:"username,omitempty"`
|
||||
Inbound string `protobuf:"bytes,4,opt,name=inbound,proto3" json:"inbound,omitempty"`
|
||||
Type string `protobuf:"bytes,5,opt,name=type,proto3" json:"type,omitempty"`
|
||||
Uuid string `protobuf:"bytes,6,opt,name=uuid,proto3" json:"uuid,omitempty"`
|
||||
Password string `protobuf:"bytes,7,opt,name=password,proto3" json:"password,omitempty"`
|
||||
Secret string `protobuf:"bytes,8,opt,name=secret,proto3" json:"secret,omitempty"`
|
||||
Flow string `protobuf:"bytes,9,opt,name=flow,proto3" json:"flow,omitempty"`
|
||||
AlterId int32 `protobuf:"varint,10,opt,name=alter_id,json=alterId,proto3" json:"alter_id,omitempty"`
|
||||
CreatedAt int64 `protobuf:"varint,11,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
|
||||
UpdatedAt int64 `protobuf:"varint,12,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
SquadIds []int32 `protobuf:"varint,2,rep,packed,name=squad_ids,json=squadIds,proto3" json:"squad_ids,omitempty"`
|
||||
Username string `protobuf:"bytes,3,opt,name=username,proto3" json:"username,omitempty"`
|
||||
Inbound string `protobuf:"bytes,4,opt,name=inbound,proto3" json:"inbound,omitempty"`
|
||||
Type string `protobuf:"bytes,5,opt,name=type,proto3" json:"type,omitempty"`
|
||||
Uuid string `protobuf:"bytes,6,opt,name=uuid,proto3" json:"uuid,omitempty"`
|
||||
Password string `protobuf:"bytes,7,opt,name=password,proto3" json:"password,omitempty"`
|
||||
Secret string `protobuf:"bytes,8,opt,name=secret,proto3" json:"secret,omitempty"`
|
||||
AuthorizedKeys []string `protobuf:"bytes,13,rep,name=authorized_keys,json=authorizedKeys,proto3" json:"authorized_keys,omitempty"`
|
||||
Flow string `protobuf:"bytes,9,opt,name=flow,proto3" json:"flow,omitempty"`
|
||||
AlterId int32 `protobuf:"varint,10,opt,name=alter_id,json=alterId,proto3" json:"alter_id,omitempty"`
|
||||
CreatedAt int64 `protobuf:"varint,11,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
|
||||
UpdatedAt int64 `protobuf:"varint,12,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *User) Reset() {
|
||||
@@ -653,6 +654,13 @@ func (x *User) GetSecret() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *User) GetAuthorizedKeys() []string {
|
||||
if x != nil {
|
||||
return x.AuthorizedKeys
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *User) GetFlow() string {
|
||||
if x != nil {
|
||||
return x.Flow
|
||||
@@ -682,18 +690,19 @@ func (x *User) GetUpdatedAt() int64 {
|
||||
}
|
||||
|
||||
type UserCreate struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
SquadIds []int32 `protobuf:"varint,1,rep,packed,name=squad_ids,json=squadIds,proto3" json:"squad_ids,omitempty"`
|
||||
Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"`
|
||||
Inbound string `protobuf:"bytes,3,opt,name=inbound,proto3" json:"inbound,omitempty"`
|
||||
Type string `protobuf:"bytes,4,opt,name=type,proto3" json:"type,omitempty"`
|
||||
Uuid string `protobuf:"bytes,5,opt,name=uuid,proto3" json:"uuid,omitempty"`
|
||||
Password string `protobuf:"bytes,6,opt,name=password,proto3" json:"password,omitempty"`
|
||||
Secret string `protobuf:"bytes,7,opt,name=secret,proto3" json:"secret,omitempty"`
|
||||
Flow string `protobuf:"bytes,8,opt,name=flow,proto3" json:"flow,omitempty"`
|
||||
AlterId int32 `protobuf:"varint,9,opt,name=alter_id,json=alterId,proto3" json:"alter_id,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
SquadIds []int32 `protobuf:"varint,1,rep,packed,name=squad_ids,json=squadIds,proto3" json:"squad_ids,omitempty"`
|
||||
Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"`
|
||||
Inbound string `protobuf:"bytes,3,opt,name=inbound,proto3" json:"inbound,omitempty"`
|
||||
Type string `protobuf:"bytes,4,opt,name=type,proto3" json:"type,omitempty"`
|
||||
Uuid string `protobuf:"bytes,5,opt,name=uuid,proto3" json:"uuid,omitempty"`
|
||||
Password string `protobuf:"bytes,6,opt,name=password,proto3" json:"password,omitempty"`
|
||||
Secret string `protobuf:"bytes,7,opt,name=secret,proto3" json:"secret,omitempty"`
|
||||
AuthorizedKeys []string `protobuf:"bytes,10,rep,name=authorized_keys,json=authorizedKeys,proto3" json:"authorized_keys,omitempty"`
|
||||
Flow string `protobuf:"bytes,8,opt,name=flow,proto3" json:"flow,omitempty"`
|
||||
AlterId int32 `protobuf:"varint,9,opt,name=alter_id,json=alterId,proto3" json:"alter_id,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *UserCreate) Reset() {
|
||||
@@ -775,6 +784,13 @@ func (x *UserCreate) GetSecret() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *UserCreate) GetAuthorizedKeys() []string {
|
||||
if x != nil {
|
||||
return x.AuthorizedKeys
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *UserCreate) GetFlow() string {
|
||||
if x != nil {
|
||||
return x.Flow
|
||||
@@ -790,14 +806,15 @@ func (x *UserCreate) GetAlterId() int32 {
|
||||
}
|
||||
|
||||
type UserUpdate struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Uuid string `protobuf:"bytes,1,opt,name=uuid,proto3" json:"uuid,omitempty"`
|
||||
Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"`
|
||||
Secret string `protobuf:"bytes,3,opt,name=secret,proto3" json:"secret,omitempty"`
|
||||
Flow string `protobuf:"bytes,4,opt,name=flow,proto3" json:"flow,omitempty"`
|
||||
AlterId int32 `protobuf:"varint,5,opt,name=alter_id,json=alterId,proto3" json:"alter_id,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Uuid string `protobuf:"bytes,1,opt,name=uuid,proto3" json:"uuid,omitempty"`
|
||||
Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"`
|
||||
Secret string `protobuf:"bytes,3,opt,name=secret,proto3" json:"secret,omitempty"`
|
||||
AuthorizedKeys []string `protobuf:"bytes,6,rep,name=authorized_keys,json=authorizedKeys,proto3" json:"authorized_keys,omitempty"`
|
||||
Flow string `protobuf:"bytes,4,opt,name=flow,proto3" json:"flow,omitempty"`
|
||||
AlterId int32 `protobuf:"varint,5,opt,name=alter_id,json=alterId,proto3" json:"alter_id,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *UserUpdate) Reset() {
|
||||
@@ -851,6 +868,13 @@ func (x *UserUpdate) GetSecret() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *UserUpdate) GetAuthorizedKeys() []string {
|
||||
if x != nil {
|
||||
return x.AuthorizedKeys
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *UserUpdate) GetFlow() string {
|
||||
if x != nil {
|
||||
return x.Flow
|
||||
@@ -2878,7 +2902,7 @@ const file_service_manager_api_grpc_manager_manager_proto_rawDesc = "" +
|
||||
"\x06values\x18\x01 \x03(\v2\x14.manager_api.v1.NodeR\x06values\"[\n" +
|
||||
"\x11NodeUpdateRequest\x12\x12\n" +
|
||||
"\x04uuid\x18\x01 \x01(\tR\x04uuid\x122\n" +
|
||||
"\x06update\x18\x02 \x01(\v2\x1a.manager_api.v1.NodeUpdateR\x06update\"\xb2\x02\n" +
|
||||
"\x06update\x18\x02 \x01(\v2\x1a.manager_api.v1.NodeUpdateR\x06update\"\xdb\x02\n" +
|
||||
"\x04User\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\x05R\x02id\x12\x1b\n" +
|
||||
"\tsquad_ids\x18\x02 \x03(\x05R\bsquadIds\x12\x1a\n" +
|
||||
@@ -2887,14 +2911,15 @@ const file_service_manager_api_grpc_manager_manager_proto_rawDesc = "" +
|
||||
"\x04type\x18\x05 \x01(\tR\x04type\x12\x12\n" +
|
||||
"\x04uuid\x18\x06 \x01(\tR\x04uuid\x12\x1a\n" +
|
||||
"\bpassword\x18\a \x01(\tR\bpassword\x12\x16\n" +
|
||||
"\x06secret\x18\b \x01(\tR\x06secret\x12\x12\n" +
|
||||
"\x06secret\x18\b \x01(\tR\x06secret\x12'\n" +
|
||||
"\x0fauthorized_keys\x18\r \x03(\tR\x0eauthorizedKeys\x12\x12\n" +
|
||||
"\x04flow\x18\t \x01(\tR\x04flow\x12\x19\n" +
|
||||
"\balter_id\x18\n" +
|
||||
" \x01(\x05R\aalterId\x12\x1d\n" +
|
||||
"\n" +
|
||||
"created_at\x18\v \x01(\x03R\tcreatedAt\x12\x1d\n" +
|
||||
"\n" +
|
||||
"updated_at\x18\f \x01(\x03R\tupdatedAt\"\xea\x01\n" +
|
||||
"updated_at\x18\f \x01(\x03R\tupdatedAt\"\x93\x02\n" +
|
||||
"\n" +
|
||||
"UserCreate\x12\x1b\n" +
|
||||
"\tsquad_ids\x18\x01 \x03(\x05R\bsquadIds\x12\x1a\n" +
|
||||
@@ -2903,14 +2928,17 @@ const file_service_manager_api_grpc_manager_manager_proto_rawDesc = "" +
|
||||
"\x04type\x18\x04 \x01(\tR\x04type\x12\x12\n" +
|
||||
"\x04uuid\x18\x05 \x01(\tR\x04uuid\x12\x1a\n" +
|
||||
"\bpassword\x18\x06 \x01(\tR\bpassword\x12\x16\n" +
|
||||
"\x06secret\x18\a \x01(\tR\x06secret\x12\x12\n" +
|
||||
"\x06secret\x18\a \x01(\tR\x06secret\x12'\n" +
|
||||
"\x0fauthorized_keys\x18\n" +
|
||||
" \x03(\tR\x0eauthorizedKeys\x12\x12\n" +
|
||||
"\x04flow\x18\b \x01(\tR\x04flow\x12\x19\n" +
|
||||
"\balter_id\x18\t \x01(\x05R\aalterId\"\x83\x01\n" +
|
||||
"\balter_id\x18\t \x01(\x05R\aalterId\"\xac\x01\n" +
|
||||
"\n" +
|
||||
"UserUpdate\x12\x12\n" +
|
||||
"\x04uuid\x18\x01 \x01(\tR\x04uuid\x12\x1a\n" +
|
||||
"\bpassword\x18\x02 \x01(\tR\bpassword\x12\x16\n" +
|
||||
"\x06secret\x18\x03 \x01(\tR\x06secret\x12\x12\n" +
|
||||
"\x06secret\x18\x03 \x01(\tR\x06secret\x12'\n" +
|
||||
"\x0fauthorized_keys\x18\x06 \x03(\tR\x0eauthorizedKeys\x12\x12\n" +
|
||||
"\x04flow\x18\x04 \x01(\tR\x04flow\x12\x19\n" +
|
||||
"\balter_id\x18\x05 \x01(\x05R\aalterId\"8\n" +
|
||||
"\bUserList\x12,\n" +
|
||||
|
||||
@@ -116,6 +116,7 @@ message User {
|
||||
string uuid = 6;
|
||||
string password = 7;
|
||||
string secret = 8;
|
||||
repeated string authorized_keys = 13;
|
||||
string flow = 9;
|
||||
int32 alter_id = 10;
|
||||
int64 created_at = 11;
|
||||
@@ -130,6 +131,7 @@ message UserCreate {
|
||||
string uuid = 5;
|
||||
string password = 6;
|
||||
string secret = 7;
|
||||
repeated string authorized_keys = 10;
|
||||
string flow = 8;
|
||||
int32 alter_id = 9;
|
||||
}
|
||||
@@ -138,6 +140,7 @@ message UserUpdate {
|
||||
string uuid = 1;
|
||||
string password = 2;
|
||||
string secret = 3;
|
||||
repeated string authorized_keys = 6;
|
||||
string flow = 4;
|
||||
int32 alter_id = 5;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.1
|
||||
// - protoc v6.31.1
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v7.34.1
|
||||
// source: service/manager_api/grpc/manager/manager.proto
|
||||
|
||||
package manager
|
||||
|
||||
@@ -42,18 +42,19 @@ func convertNode(v CM.Node) *pb.Node {
|
||||
|
||||
func convertUser(v CM.User) *pb.User {
|
||||
return &pb.User{
|
||||
Id: int32(v.ID),
|
||||
SquadIds: toInt32Slice(v.SquadIDs),
|
||||
Username: v.Username,
|
||||
Inbound: v.Inbound,
|
||||
Type: v.Type,
|
||||
Uuid: v.UUID,
|
||||
Password: v.Password,
|
||||
Secret: v.Secret,
|
||||
Flow: v.Flow,
|
||||
AlterId: int32(v.AlterID),
|
||||
CreatedAt: v.CreatedAt.UnixNano(),
|
||||
UpdatedAt: v.UpdatedAt.UnixNano(),
|
||||
Id: int32(v.ID),
|
||||
SquadIds: toInt32Slice(v.SquadIDs),
|
||||
Username: v.Username,
|
||||
Inbound: v.Inbound,
|
||||
Type: v.Type,
|
||||
Uuid: v.UUID,
|
||||
Password: v.Password,
|
||||
Secret: v.Secret,
|
||||
AuthorizedKeys: v.AuthorizedKeys,
|
||||
Flow: v.Flow,
|
||||
AlterId: int32(v.AlterID),
|
||||
CreatedAt: v.CreatedAt.UnixNano(),
|
||||
UpdatedAt: v.UpdatedAt.UnixNano(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +67,7 @@ func convertBandwidthLimiter(v CM.BandwidthLimiter) *pb.BandwidthLimiter {
|
||||
Strategy: v.Strategy,
|
||||
ConnectionType: v.ConnectionType,
|
||||
Mode: v.Mode,
|
||||
FlowKeys: v.FlowKeys,
|
||||
FlowKeys: v.FlowKeys,
|
||||
Speed: v.Speed,
|
||||
RawSpeed: v.RawSpeed,
|
||||
CreatedAt: v.CreatedAt.UnixNano(),
|
||||
|
||||
@@ -125,15 +125,16 @@ func (s *Server) DeleteNode(_ context.Context, req *pb.UuidRequest) (*pb.Node, e
|
||||
|
||||
func (s *Server) CreateUser(_ context.Context, req *pb.UserCreate) (*pb.User, error) {
|
||||
v, err := s.manager.CreateUser(CM.UserCreate{
|
||||
SquadIDs: toIntSlice(req.GetSquadIds()),
|
||||
Username: req.GetUsername(),
|
||||
Inbound: req.GetInbound(),
|
||||
Type: req.GetType(),
|
||||
UUID: req.GetUuid(),
|
||||
Password: req.GetPassword(),
|
||||
Secret: req.GetSecret(),
|
||||
Flow: req.GetFlow(),
|
||||
AlterID: int(req.GetAlterId()),
|
||||
SquadIDs: toIntSlice(req.GetSquadIds()),
|
||||
Username: req.GetUsername(),
|
||||
Inbound: req.GetInbound(),
|
||||
Type: req.GetType(),
|
||||
UUID: req.GetUuid(),
|
||||
Password: req.GetPassword(),
|
||||
Secret: req.GetSecret(),
|
||||
AuthorizedKeys: req.GetAuthorizedKeys(),
|
||||
Flow: req.GetFlow(),
|
||||
AlterID: int(req.GetAlterId()),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -172,11 +173,12 @@ func (s *Server) GetUser(_ context.Context, req *pb.IdRequest) (*pb.User, error)
|
||||
func (s *Server) UpdateUser(_ context.Context, req *pb.UserUpdateRequest) (*pb.User, error) {
|
||||
u := req.GetUpdate()
|
||||
v, err := s.manager.UpdateUser(int(req.GetId()), CM.UserUpdate{
|
||||
UUID: u.GetUuid(),
|
||||
Password: u.GetPassword(),
|
||||
Secret: u.GetSecret(),
|
||||
Flow: u.GetFlow(),
|
||||
AlterID: int(u.GetAlterId()),
|
||||
UUID: u.GetUuid(),
|
||||
Password: u.GetPassword(),
|
||||
Secret: u.GetSecret(),
|
||||
AuthorizedKeys: u.GetAuthorizedKeys(),
|
||||
Flow: u.GetFlow(),
|
||||
AlterID: int(u.GetAlterId()),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -200,7 +202,7 @@ func (s *Server) CreateBandwidthLimiter(_ context.Context, req *pb.BandwidthLimi
|
||||
Strategy: req.GetStrategy(),
|
||||
ConnectionType: req.GetConnectionType(),
|
||||
Mode: req.GetMode(),
|
||||
FlowKeys: req.GetFlowKeys(),
|
||||
FlowKeys: req.GetFlowKeys(),
|
||||
Speed: req.GetSpeed(),
|
||||
})
|
||||
if err != nil {
|
||||
@@ -245,7 +247,7 @@ func (s *Server) UpdateBandwidthLimiter(_ context.Context, req *pb.BandwidthLimi
|
||||
Strategy: u.GetStrategy(),
|
||||
ConnectionType: u.GetConnectionType(),
|
||||
Mode: u.GetMode(),
|
||||
FlowKeys: u.GetFlowKeys(),
|
||||
FlowKeys: u.GetFlowKeys(),
|
||||
Speed: u.GetSpeed(),
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -194,7 +194,7 @@ paths:
|
||||
- {in: query, name: id, schema: {type: integer, format: int32}}
|
||||
- {in: query, name: username, schema: {type: string}}
|
||||
- {in: query, name: inbound, schema: {type: string}}
|
||||
- {in: query, name: type, schema: {type: string, enum: [hysteria, hysteria2, mtproxy, trojan, tuic, vless, vmess]}}
|
||||
- {in: query, name: type, schema: {type: string, enum: [anytls, http, hysteria, hysteria2, mixed, mtproxy, naive, socks, ssh, trojan, trusttunnel, tuic, vless, vmess]}}
|
||||
- {$ref: "#/components/parameters/FilterSquadIdIn"}
|
||||
- {$ref: "#/components/parameters/FilterCreatedAtStart"}
|
||||
- {$ref: "#/components/parameters/FilterCreatedAtEnd"}
|
||||
@@ -210,6 +210,15 @@ paths:
|
||||
post:
|
||||
tags: [Users]
|
||||
summary: Create user
|
||||
description: |
|
||||
Required fields depend on `type`:
|
||||
- **vless**: uuid (flow optional)
|
||||
- **vmess**: uuid, alter_id
|
||||
- **trojan, hysteria, hysteria2**: password
|
||||
- **tuic**: uuid, password
|
||||
- **mtproxy**: secret
|
||||
- **ssh**: password OR authorized_keys (at least one)
|
||||
- **anytls, http, mixed, naive, socks, trusttunnel**: password
|
||||
requestBody: {required: true, content: {application/json: {schema: {$ref: "#/components/schemas/UserCreate"}}}}
|
||||
responses:
|
||||
"201": {content: {application/json: {schema: {$ref: "#/components/schemas/User"}}}}
|
||||
@@ -222,7 +231,7 @@ paths:
|
||||
- {in: query, name: id, schema: {type: integer, format: int32}}
|
||||
- {in: query, name: username, schema: {type: string}}
|
||||
- {in: query, name: inbound, schema: {type: string}}
|
||||
- {in: query, name: type, schema: {type: string, enum: [hysteria, hysteria2, mtproxy, trojan, tuic, vless, vmess]}}
|
||||
- {in: query, name: type, schema: {type: string, enum: [anytls, http, hysteria, hysteria2, mixed, mtproxy, naive, socks, ssh, trojan, trusttunnel, tuic, vless, vmess]}}
|
||||
- {$ref: "#/components/parameters/FilterSquadIdIn"}
|
||||
- {$ref: "#/components/parameters/FilterCreatedAtStart"}
|
||||
- {$ref: "#/components/parameters/FilterCreatedAtEnd"}
|
||||
@@ -247,6 +256,15 @@ paths:
|
||||
put:
|
||||
tags: [Users]
|
||||
summary: Update user
|
||||
description: |
|
||||
Required fields depend on user `type`:
|
||||
- **vless**: uuid (flow optional)
|
||||
- **vmess**: uuid, alter_id
|
||||
- **trojan, hysteria, hysteria2**: password
|
||||
- **tuic**: uuid, password
|
||||
- **mtproxy**: secret
|
||||
- **ssh**: password OR authorized_keys (at least one)
|
||||
- **anytls, http, mixed, naive, socks, trusttunnel**: password
|
||||
requestBody: {required: true, content: {application/json: {schema: {$ref: "#/components/schemas/UserUpdate"}}}}
|
||||
responses:
|
||||
"200": {content: {application/json: {schema: {$ref: "#/components/schemas/User"}}}}
|
||||
@@ -266,7 +284,7 @@ paths:
|
||||
summary: List bandwidth limiters
|
||||
parameters:
|
||||
- {in: query, name: id, schema: {type: integer, format: int32}}
|
||||
- {in: query, name: strategy, schema: {type: string, enum: [global, connection]}}
|
||||
- {in: query, name: strategy, schema: {type: string, enum: [global, connection, bypass]}}
|
||||
- {in: query, name: mode, schema: {type: string, enum: [upload, download, bidirectional]}}
|
||||
- {in: query, name: type, schema: {type: string}}
|
||||
- {in: query, name: username, schema: {type: string}}
|
||||
@@ -299,7 +317,7 @@ paths:
|
||||
summary: Count bandwidth limiters
|
||||
parameters:
|
||||
- {in: query, name: id, schema: {type: integer, format: int32}}
|
||||
- {in: query, name: strategy, schema: {type: string, enum: [global, connection]}}
|
||||
- {in: query, name: strategy, schema: {type: string, enum: [global, connection, bypass]}}
|
||||
- {in: query, name: mode, schema: {type: string, enum: [upload, download, bidirectional]}}
|
||||
- {in: query, name: type, schema: {type: string}}
|
||||
- {in: query, name: username, schema: {type: string}}
|
||||
@@ -430,7 +448,7 @@ paths:
|
||||
summary: List connection limiters
|
||||
parameters:
|
||||
- {in: query, name: id, schema: {type: integer, format: int32}}
|
||||
- {in: query, name: strategy, schema: {type: string, enum: [connection]}}
|
||||
- {in: query, name: strategy, schema: {type: string, enum: [connection, bypass]}}
|
||||
- {in: query, name: username, schema: {type: string}}
|
||||
- {in: query, name: outbound, schema: {type: string}}
|
||||
- {in: query, name: connection_type, schema: {type: string, enum: [default, hwid, mux, ip]}}
|
||||
@@ -460,7 +478,7 @@ paths:
|
||||
summary: Count connection limiters
|
||||
parameters:
|
||||
- {in: query, name: id, schema: {type: integer, format: int32}}
|
||||
- {in: query, name: strategy, schema: {type: string, enum: [connection]}}
|
||||
- {in: query, name: strategy, schema: {type: string, enum: [connection, bypass]}}
|
||||
- {in: query, name: username, schema: {type: string}}
|
||||
- {in: query, name: outbound, schema: {type: string}}
|
||||
- {in: query, name: connection_type, schema: {type: string, enum: [default, hwid, mux, ip]}}
|
||||
@@ -507,7 +525,7 @@ paths:
|
||||
summary: List rate limiters
|
||||
parameters:
|
||||
- {in: query, name: id, schema: {type: integer, format: int32}}
|
||||
- {in: query, name: strategy, schema: {type: string, enum: [fixed_window, sliding_window, token_bucket, leaky_bucket]}}
|
||||
- {in: query, name: strategy, schema: {type: string, enum: [fixed_window, sliding_window, token_bucket, leaky_bucket, bypass]}}
|
||||
- {in: query, name: username, schema: {type: string}}
|
||||
- {in: query, name: outbound, schema: {type: string}}
|
||||
- {in: query, name: connection_type, schema: {type: string, enum: [hwid, mux, ip, default]}}
|
||||
@@ -539,7 +557,7 @@ paths:
|
||||
summary: Count rate limiters
|
||||
parameters:
|
||||
- {in: query, name: id, schema: {type: integer, format: int32}}
|
||||
- {in: query, name: strategy, schema: {type: string, enum: [fixed_window, sliding_window, token_bucket, leaky_bucket]}}
|
||||
- {in: query, name: strategy, schema: {type: string, enum: [fixed_window, sliding_window, token_bucket, leaky_bucket, bypass]}}
|
||||
- {in: query, name: username, schema: {type: string}}
|
||||
- {in: query, name: outbound, schema: {type: string}}
|
||||
- {in: query, name: connection_type, schema: {type: string, enum: [hwid, mux, ip, default]}}
|
||||
@@ -686,16 +704,17 @@ components:
|
||||
|
||||
User:
|
||||
type: object
|
||||
required: [id, squad_ids, username, inbound, type, uuid, password, secret, flow, alter_id, created_at, updated_at]
|
||||
required: [id, squad_ids, username, inbound, type, uuid, password, secret, authorized_keys, flow, alter_id, created_at, updated_at]
|
||||
properties:
|
||||
id: {type: integer, format: int32}
|
||||
squad_ids: {$ref: "#/components/schemas/SquadIDs"}
|
||||
username: {type: string, example: "alice"}
|
||||
inbound: {type: string, example: "vless-in"}
|
||||
type: {type: string, enum: [hysteria, hysteria2, mtproxy, trojan, tuic, vless, vmess]}
|
||||
type: {type: string, enum: [anytls, http, hysteria, hysteria2, mixed, mtproxy, naive, socks, ssh, trojan, trusttunnel, tuic, vless, vmess]}
|
||||
uuid: {type: string}
|
||||
password: {type: string}
|
||||
secret: {type: string}
|
||||
authorized_keys: {type: array, items: {type: string}}
|
||||
flow: {type: string}
|
||||
alter_id: {type: integer, format: int32}
|
||||
created_at: {type: string, format: date-time}
|
||||
@@ -703,24 +722,44 @@ components:
|
||||
UserCreate:
|
||||
type: object
|
||||
required: [squad_ids, username, inbound, type]
|
||||
description: |
|
||||
Required fields depend on `type`:
|
||||
- vless: uuid (flow optional)
|
||||
- vmess: uuid, alter_id
|
||||
- trojan, shadowsocks, hysteria, hysteria2: password
|
||||
- tuic: uuid, password
|
||||
- mtproxy: secret
|
||||
- ssh: password OR authorized_keys (at least one)
|
||||
- anytls, http, mixed, naive, socks, trusttunnel: password
|
||||
properties:
|
||||
squad_ids: {$ref: "#/components/schemas/SquadIDs"}
|
||||
username: {type: string, example: "alice"}
|
||||
inbound: {type: string, example: "vless-in"}
|
||||
type:
|
||||
type: string
|
||||
enum: [hysteria, hysteria2, mtproxy, trojan, tuic, vless, vmess]
|
||||
enum: [anytls, http, hysteria, hysteria2, mixed, mtproxy, naive, socks, ssh, trojan, trusttunnel, tuic, vless, vmess]
|
||||
uuid: {type: string, format: uuid}
|
||||
password: {type: string}
|
||||
secret: {type: string}
|
||||
authorized_keys: {type: array, items: {type: string}}
|
||||
flow: {type: string}
|
||||
alter_id: {type: integer, format: int32}
|
||||
UserUpdate:
|
||||
type: object
|
||||
description: |
|
||||
All fields are optional. Validation rules match UserCreate by type:
|
||||
- vless: uuid (flow optional)
|
||||
- vmess: uuid, alter_id
|
||||
- trojan, shadowsocks, hysteria, hysteria2: password
|
||||
- tuic: uuid, password
|
||||
- mtproxy: secret
|
||||
- ssh: password OR authorized_keys (at least one)
|
||||
- anytls, http, mixed, naive, socks, trusttunnel: password
|
||||
properties:
|
||||
uuid: {type: string, format: uuid}
|
||||
password: {type: string}
|
||||
secret: {type: string}
|
||||
authorized_keys: {type: array, items: {type: string}}
|
||||
flow: {type: string}
|
||||
alter_id: {type: integer, format: int32}
|
||||
|
||||
@@ -732,41 +771,43 @@ components:
|
||||
squad_ids: {$ref: "#/components/schemas/SquadIDs"}
|
||||
username: {type: string}
|
||||
outbound: {type: string, example: "direct"}
|
||||
strategy: {type: string, enum: [global, connection]}
|
||||
connection_type: {type: string, enum: [default, hwid, mux, ip]}
|
||||
strategy: {type: string, enum: [global, connection, bypass]}
|
||||
connection_type: {type: string}
|
||||
mode: {type: string, enum: [upload, download, bidirectional]}
|
||||
flow_keys: {type: array, items: {type: string, enum: [user, destination, ip, hwid, mux]}}
|
||||
flow_keys: {type: array, items: {type: string, enum: [user, source_ip, hwid, mux, protocol, destination]}}
|
||||
speed: {type: string, example: "10mbit"}
|
||||
raw_speed: {type: integer, format: int64}
|
||||
created_at: {type: string, format: date-time}
|
||||
updated_at: {type: string, format: date-time}
|
||||
BandwidthLimiterCreate:
|
||||
type: object
|
||||
required: [squad_ids, outbound, strategy, mode, speed]
|
||||
required: [squad_ids, outbound, strategy]
|
||||
description: "mode, speed, flow_keys, connection_type are required/relevant unless strategy=bypass"
|
||||
properties:
|
||||
squad_ids: {$ref: "#/components/schemas/SquadIDs"}
|
||||
username: {type: string}
|
||||
outbound: {type: string, example: "direct"}
|
||||
strategy: {type: string, enum: [global, connection]}
|
||||
connection_type: {type: string, enum: [default, hwid, mux, ip]}
|
||||
strategy: {type: string, enum: [global, connection, bypass]}
|
||||
connection_type: {type: string}
|
||||
mode: {type: string, enum: [upload, download, bidirectional]}
|
||||
flow_keys: {type: array, items: {type: string, enum: [user, destination, ip, hwid, mux]}}
|
||||
flow_keys: {type: array, items: {type: string, enum: [user, source_ip, hwid, mux, protocol, destination]}}
|
||||
speed: {type: string, example: "10mbit"}
|
||||
BandwidthLimiterUpdate:
|
||||
type: object
|
||||
required: [outbound, strategy, mode, speed]
|
||||
required: [outbound, strategy]
|
||||
description: "mode, speed, flow_keys, connection_type are required/relevant unless strategy=bypass"
|
||||
properties:
|
||||
username: {type: string}
|
||||
outbound: {type: string}
|
||||
strategy: {type: string, enum: [global, connection]}
|
||||
connection_type: {type: string, enum: [default, hwid, mux, ip]}
|
||||
strategy: {type: string, enum: [global, connection, bypass]}
|
||||
connection_type: {type: string}
|
||||
mode: {type: string, enum: [upload, download, bidirectional]}
|
||||
flow_keys: {type: array, items: {type: string, enum: [user, destination, ip, hwid, mux]}}
|
||||
flow_keys: {type: array, items: {type: string, enum: [user, source_ip, hwid, mux, protocol, destination]}}
|
||||
speed: {type: string}
|
||||
|
||||
TrafficLimiter:
|
||||
type: object
|
||||
required: [id, squad_ids, outbound, strategy, mode, raw_used, quota, raw_quota, created_at, updated_at]
|
||||
required: [id, squad_ids, outbound, strategy, mode, raw_used, quota, raw_quota, usage, created_at, updated_at]
|
||||
properties:
|
||||
id: {type: integer, format: int32}
|
||||
squad_ids: {$ref: "#/components/schemas/SquadIDs"}
|
||||
@@ -777,11 +818,13 @@ components:
|
||||
raw_used: {type: integer, format: int64}
|
||||
quota: {type: string, example: "10gb"}
|
||||
raw_quota: {type: integer, format: int64}
|
||||
usage: {type: integer, format: int32, description: "Usage percentage 0-100"}
|
||||
created_at: {type: string, format: date-time}
|
||||
updated_at: {type: string, format: date-time}
|
||||
TrafficLimiterCreate:
|
||||
type: object
|
||||
required: [squad_ids, outbound, strategy, mode, quota]
|
||||
required: [squad_ids, outbound, strategy]
|
||||
description: "mode, quota are required unless strategy=bypass"
|
||||
properties:
|
||||
squad_ids: {$ref: "#/components/schemas/SquadIDs"}
|
||||
username: {type: string}
|
||||
@@ -791,7 +834,8 @@ components:
|
||||
quota: {type: string, example: "10gb"}
|
||||
TrafficLimiterUpdate:
|
||||
type: object
|
||||
required: [outbound, strategy, mode, quota]
|
||||
required: [outbound, strategy]
|
||||
description: "mode, quota are required unless strategy=bypass"
|
||||
properties:
|
||||
username: {type: string}
|
||||
outbound: {type: string}
|
||||
@@ -807,7 +851,7 @@ components:
|
||||
squad_ids: {$ref: "#/components/schemas/SquadIDs"}
|
||||
username: {type: string}
|
||||
outbound: {type: string, example: "direct"}
|
||||
strategy: {type: string, enum: [connection]}
|
||||
strategy: {type: string, enum: [connection, bypass]}
|
||||
connection_type: {type: string, enum: [default, hwid, mux, ip]}
|
||||
lock_type: {type: string, enum: [manager, default]}
|
||||
count: {type: integer, format: int64}
|
||||
@@ -815,22 +859,24 @@ components:
|
||||
updated_at: {type: string, format: date-time}
|
||||
ConnectionLimiterCreate:
|
||||
type: object
|
||||
required: [squad_ids, outbound, strategy, lock_type, count]
|
||||
required: [squad_ids, outbound, strategy]
|
||||
description: "lock_type, connection_type, count are required unless strategy=bypass"
|
||||
properties:
|
||||
squad_ids: {$ref: "#/components/schemas/SquadIDs"}
|
||||
username: {type: string}
|
||||
outbound: {type: string, example: "direct"}
|
||||
strategy: {type: string, enum: [connection]}
|
||||
strategy: {type: string, enum: [connection, bypass]}
|
||||
connection_type: {type: string, enum: [default, hwid, mux, ip]}
|
||||
lock_type: {type: string, enum: [manager, default]}
|
||||
count: {type: integer, format: int64}
|
||||
ConnectionLimiterUpdate:
|
||||
type: object
|
||||
required: [outbound, strategy, lock_type, count]
|
||||
required: [outbound, strategy]
|
||||
description: "lock_type, connection_type, count are required unless strategy=bypass"
|
||||
properties:
|
||||
username: {type: string}
|
||||
outbound: {type: string}
|
||||
strategy: {type: string, enum: [connection]}
|
||||
strategy: {type: string, enum: [connection, bypass]}
|
||||
connection_type: {type: string, enum: [default, hwid, mux, ip]}
|
||||
lock_type: {type: string, enum: [manager, default]}
|
||||
count: {type: integer, format: int64}
|
||||
@@ -843,7 +889,7 @@ components:
|
||||
squad_ids: {$ref: "#/components/schemas/SquadIDs"}
|
||||
username: {type: string}
|
||||
outbound: {type: string, example: "direct"}
|
||||
strategy: {type: string, enum: [fixed_window, sliding_window, token_bucket, leaky_bucket]}
|
||||
strategy: {type: string, enum: [fixed_window, sliding_window, token_bucket, leaky_bucket, bypass]}
|
||||
connection_type: {type: string, enum: [hwid, mux, ip, default]}
|
||||
count: {type: integer, format: int64}
|
||||
interval: {type: string, example: "1s"}
|
||||
@@ -851,22 +897,24 @@ components:
|
||||
updated_at: {type: string, format: date-time}
|
||||
RateLimiterCreate:
|
||||
type: object
|
||||
required: [squad_ids, outbound, strategy, connection_type, count, interval]
|
||||
required: [squad_ids, outbound, strategy]
|
||||
description: "connection_type, count, interval are required unless strategy=bypass"
|
||||
properties:
|
||||
squad_ids: {$ref: "#/components/schemas/SquadIDs"}
|
||||
username: {type: string}
|
||||
outbound: {type: string, example: "direct"}
|
||||
strategy: {type: string, enum: [fixed_window, sliding_window, token_bucket, leaky_bucket]}
|
||||
strategy: {type: string, enum: [fixed_window, sliding_window, token_bucket, leaky_bucket, bypass]}
|
||||
connection_type: {type: string, enum: [hwid, mux, ip, default]}
|
||||
count: {type: integer, format: int64}
|
||||
interval: {type: string, example: "1s"}
|
||||
RateLimiterUpdate:
|
||||
type: object
|
||||
required: [outbound, strategy, connection_type, count, interval]
|
||||
required: [outbound, strategy]
|
||||
description: "connection_type, count, interval are required unless strategy=bypass"
|
||||
properties:
|
||||
username: {type: string}
|
||||
outbound: {type: string}
|
||||
strategy: {type: string, enum: [fixed_window, sliding_window, token_bucket, leaky_bucket]}
|
||||
strategy: {type: string, enum: [fixed_window, sliding_window, token_bucket, leaky_bucket, bypass]}
|
||||
connection_type: {type: string, enum: [hwid, mux, ip, default]}
|
||||
count: {type: integer, format: int64}
|
||||
interval: {type: string}
|
||||
|
||||
97
service/node/inbound/ssh.go
Normal file
97
service/node/inbound/ssh.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package inbound
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-box/protocol/ssh"
|
||||
CM "github.com/sagernet/sing-box/service/manager/constant"
|
||||
"github.com/sagernet/sing-box/service/node/constant"
|
||||
)
|
||||
|
||||
type SSHManager struct {
|
||||
inbounds map[string]*SSHUserManager
|
||||
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
func NewSSHManager() *SSHManager {
|
||||
return &SSHManager{
|
||||
inbounds: make(map[string]*SSHUserManager),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *SSHManager) AddUserManager(inbound adapter.Inbound) error {
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
m.inbounds[inbound.Tag()] = &SSHUserManager{
|
||||
inbound: inbound.(*ssh.Inbound),
|
||||
usersMap: make(map[string]option.SSHUser),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *SSHManager) GetUserManager(tag string) (constant.UserManager, bool) {
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
inbound, ok := m.inbounds[tag]
|
||||
return inbound, ok
|
||||
}
|
||||
|
||||
func (m *SSHManager) GetUserManagerTags() []string {
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
tags := make([]string, 0, len(m.inbounds))
|
||||
for tag := range m.inbounds {
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
type SSHUserManager struct {
|
||||
inbound *ssh.Inbound
|
||||
usersMap map[string]option.SSHUser
|
||||
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
func (i *SSHUserManager) postUpdate() {
|
||||
users := make([]option.SSHUser, 0, len(i.usersMap))
|
||||
for _, user := range i.usersMap {
|
||||
users = append(users, user)
|
||||
}
|
||||
i.inbound.UpdateUsers(users)
|
||||
}
|
||||
|
||||
func convertSSHUser(user CM.User) option.SSHUser {
|
||||
return option.SSHUser{
|
||||
Name: user.Username,
|
||||
Password: user.Password,
|
||||
AuthorizedKeys: user.AuthorizedKeys,
|
||||
}
|
||||
}
|
||||
|
||||
func (i *SSHUserManager) UpdateUser(user CM.User) {
|
||||
i.mtx.Lock()
|
||||
defer i.mtx.Unlock()
|
||||
i.usersMap[user.Username] = convertSSHUser(user)
|
||||
i.postUpdate()
|
||||
}
|
||||
|
||||
func (i *SSHUserManager) UpdateUsers(users []CM.User) {
|
||||
i.mtx.Lock()
|
||||
defer i.mtx.Unlock()
|
||||
clear(i.usersMap)
|
||||
for _, user := range users {
|
||||
i.usersMap[user.Username] = convertSSHUser(user)
|
||||
}
|
||||
i.postUpdate()
|
||||
}
|
||||
|
||||
func (i *SSHUserManager) DeleteUser(username string) {
|
||||
i.mtx.Lock()
|
||||
defer i.mtx.Unlock()
|
||||
delete(i.usersMap, username)
|
||||
i.postUpdate()
|
||||
}
|
||||
@@ -33,7 +33,7 @@ type Service struct {
|
||||
rateManager constant.RateLimiterManager
|
||||
options option.NodeServiceOptions
|
||||
|
||||
nodeManager CM.NodeManager
|
||||
nodeManager CM.NodeManager
|
||||
|
||||
mtx sync.Mutex
|
||||
}
|
||||
@@ -79,6 +79,7 @@ func (s *Service) Start(stage adapter.StartStage) error {
|
||||
"tuic": inbound.NewTUICManager(),
|
||||
"vless": inbound.NewVLESSManager(),
|
||||
"vmess": inbound.NewVMessManager(),
|
||||
"ssh": inbound.NewSSHManager(),
|
||||
}
|
||||
s.connectionManager = limiter.NewConnectionLimiterManager(s.ctx, nodeManager, s.logger)
|
||||
s.bandwidthManager = limiter.NewBandwidthLimiterManager(s.ctx, nodeManager, s.logger)
|
||||
|
||||
@@ -281,15 +281,16 @@ func (s *APIClient) handler(node CM.ConnectedNode, stream grpc.ServerStreamingCl
|
||||
|
||||
func (s *APIClient) convertUser(user *pb.User) CM.User {
|
||||
return CM.User{
|
||||
ID: int(user.Id),
|
||||
Username: user.Username,
|
||||
Inbound: user.Inbound,
|
||||
Type: user.Type,
|
||||
UUID: user.Uuid,
|
||||
Password: user.Password,
|
||||
Secret: user.Secret,
|
||||
Flow: user.Flow,
|
||||
AlterID: int(user.AlterId),
|
||||
ID: int(user.Id),
|
||||
Username: user.Username,
|
||||
Inbound: user.Inbound,
|
||||
Type: user.Type,
|
||||
UUID: user.Uuid,
|
||||
Password: user.Password,
|
||||
Secret: user.Secret,
|
||||
AuthorizedKeys: user.AuthorizedKeys,
|
||||
Flow: user.Flow,
|
||||
AlterID: int(user.AlterId),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,7 +302,7 @@ func (s *APIClient) convertBandwidthLimiter(limiter *pb.BandwidthLimiter) CM.Ban
|
||||
Strategy: limiter.Strategy,
|
||||
ConnectionType: limiter.ConnectionType,
|
||||
Mode: limiter.Mode,
|
||||
FlowKeys: limiter.FlowKeys,
|
||||
FlowKeys: limiter.FlowKeys,
|
||||
Speed: limiter.Speed,
|
||||
RawSpeed: limiter.RawSpeed,
|
||||
}
|
||||
|
||||
@@ -151,18 +151,19 @@ func (x *Node) GetUuid() string {
|
||||
}
|
||||
|
||||
type User struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"`
|
||||
Inbound string `protobuf:"bytes,3,opt,name=inbound,proto3" json:"inbound,omitempty"`
|
||||
Type string `protobuf:"bytes,4,opt,name=type,proto3" json:"type,omitempty"`
|
||||
Uuid string `protobuf:"bytes,5,opt,name=uuid,proto3" json:"uuid,omitempty"`
|
||||
Password string `protobuf:"bytes,6,opt,name=password,proto3" json:"password,omitempty"`
|
||||
Secret string `protobuf:"bytes,7,opt,name=secret,proto3" json:"secret,omitempty"`
|
||||
Flow string `protobuf:"bytes,8,opt,name=flow,proto3" json:"flow,omitempty"`
|
||||
AlterId int32 `protobuf:"varint,9,opt,name=alter_id,json=alterId,proto3" json:"alter_id,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"`
|
||||
Inbound string `protobuf:"bytes,3,opt,name=inbound,proto3" json:"inbound,omitempty"`
|
||||
Type string `protobuf:"bytes,4,opt,name=type,proto3" json:"type,omitempty"`
|
||||
Uuid string `protobuf:"bytes,5,opt,name=uuid,proto3" json:"uuid,omitempty"`
|
||||
Password string `protobuf:"bytes,6,opt,name=password,proto3" json:"password,omitempty"`
|
||||
Secret string `protobuf:"bytes,7,opt,name=secret,proto3" json:"secret,omitempty"`
|
||||
Flow string `protobuf:"bytes,8,opt,name=flow,proto3" json:"flow,omitempty"`
|
||||
AlterId int32 `protobuf:"varint,9,opt,name=alter_id,json=alterId,proto3" json:"alter_id,omitempty"`
|
||||
AuthorizedKeys []string `protobuf:"bytes,10,rep,name=authorized_keys,json=authorizedKeys,proto3" json:"authorized_keys,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *User) Reset() {
|
||||
@@ -258,6 +259,13 @@ func (x *User) GetAlterId() int32 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *User) GetAuthorizedKeys() []string {
|
||||
if x != nil {
|
||||
return x.AuthorizedKeys
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type UserList struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Values []*User `protobuf:"bytes,1,rep,name=values,proto3" json:"values,omitempty"`
|
||||
@@ -1338,7 +1346,7 @@ const file_service_node_manager_api_manager_manager_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
".service/node_manager_api/manager/manager.proto\x12\x13node_manager_api.v1\"\x1a\n" +
|
||||
"\x04Node\x12\x12\n" +
|
||||
"\x04uuid\x18\x01 \x01(\tR\x04uuid\"\xd7\x01\n" +
|
||||
"\x04uuid\x18\x01 \x01(\tR\x04uuid\"\x80\x02\n" +
|
||||
"\x04User\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\x05R\x02id\x12\x1a\n" +
|
||||
"\busername\x18\x02 \x01(\tR\busername\x12\x18\n" +
|
||||
@@ -1348,7 +1356,9 @@ const file_service_node_manager_api_manager_manager_proto_rawDesc = "" +
|
||||
"\bpassword\x18\x06 \x01(\tR\bpassword\x12\x16\n" +
|
||||
"\x06secret\x18\a \x01(\tR\x06secret\x12\x12\n" +
|
||||
"\x04flow\x18\b \x01(\tR\x04flow\x12\x19\n" +
|
||||
"\balter_id\x18\t \x01(\x05R\aalterId\"=\n" +
|
||||
"\balter_id\x18\t \x01(\x05R\aalterId\x12'\n" +
|
||||
"\x0fauthorized_keys\x18\n" +
|
||||
" \x03(\tR\x0eauthorizedKeys\"=\n" +
|
||||
"\bUserList\x121\n" +
|
||||
"\x06values\x18\x01 \x03(\v2\x19.node_manager_api.v1.UserR\x06values\"\x83\x02\n" +
|
||||
"\x10BandwidthLimiter\x12\x0e\n" +
|
||||
|
||||
@@ -48,6 +48,7 @@ message User {
|
||||
string secret = 7;
|
||||
string flow = 8;
|
||||
int32 alter_id = 9;
|
||||
repeated string authorized_keys = 10;
|
||||
}
|
||||
|
||||
message UserList {
|
||||
|
||||
@@ -239,15 +239,16 @@ func (s *RemoteNode) close(err error) {
|
||||
|
||||
func (s *RemoteNode) convertUser(user CS.User) *pb.User {
|
||||
return &pb.User{
|
||||
Id: int32(user.ID),
|
||||
Username: user.Username,
|
||||
Inbound: user.Inbound,
|
||||
Type: user.Type,
|
||||
Uuid: user.UUID,
|
||||
Password: user.Password,
|
||||
Secret: user.Secret,
|
||||
Flow: user.Flow,
|
||||
AlterId: int32(user.AlterID),
|
||||
Id: int32(user.ID),
|
||||
Username: user.Username,
|
||||
Inbound: user.Inbound,
|
||||
Type: user.Type,
|
||||
Uuid: user.UUID,
|
||||
Password: user.Password,
|
||||
Secret: user.Secret,
|
||||
AuthorizedKeys: user.AuthorizedKeys,
|
||||
Flow: user.Flow,
|
||||
AlterId: int32(user.AlterID),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -98,12 +98,12 @@ func (s *Service) Start(stage adapter.StartStage) error {
|
||||
s.adaptiveTimer = newAdaptiveTimer(s.logger, s.router, s.timerConfig)
|
||||
s.adaptiveTimer.start(false)
|
||||
if s.memoryLimit > 0 {
|
||||
s.logger.Info("started memory monitor with limit: ", s.memoryLimit/(1024*1024), " MiB")
|
||||
s.logger.Notice("started memory monitor with limit: ", s.memoryLimit/(1024*1024), " MiB")
|
||||
} else {
|
||||
s.logger.Info("started memory monitor with available memory detection")
|
||||
s.logger.Notice("started memory monitor with available memory detection")
|
||||
}
|
||||
} else {
|
||||
s.logger.Info("started memory pressure monitor")
|
||||
s.logger.Notice("started memory pressure monitor")
|
||||
}
|
||||
|
||||
globalAccess.Lock()
|
||||
|
||||
@@ -66,9 +66,9 @@ func (s *Service) Start(stage adapter.StartStage) error {
|
||||
s.adaptiveTimer = newAdaptiveTimer(s.logger, s.router, s.timerConfig)
|
||||
s.adaptiveTimer.start(false)
|
||||
if s.useAvailable {
|
||||
s.logger.Info("started memory monitor with available memory detection")
|
||||
s.logger.Notice("started memory monitor with available memory detection")
|
||||
} else {
|
||||
s.logger.Info("started memory monitor with limit: ", s.memoryLimit/(1024*1024), " MiB")
|
||||
s.logger.Notice("started memory monitor with limit: ", s.memoryLimit/(1024*1024), " MiB")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -26,7 +26,10 @@ import (
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
goleak.VerifyTestMain(m)
|
||||
goleak.VerifyTestMain(m,
|
||||
goleak.IgnoreTopFunction("github.com/panjf2000/ants/v2.(*poolCommon).purgeStaleWorkers"),
|
||||
goleak.IgnoreTopFunction("github.com/panjf2000/ants/v2.(*poolCommon).ticktock"),
|
||||
)
|
||||
}
|
||||
|
||||
var globalCtx context.Context
|
||||
|
||||
@@ -36,6 +36,7 @@ require (
|
||||
github.com/spyzhov/ajson v0.9.4
|
||||
github.com/stretchr/testify v1.11.1
|
||||
go.uber.org/goleak v1.3.0
|
||||
golang.org/x/crypto v0.49.0
|
||||
golang.org/x/net v0.52.0
|
||||
)
|
||||
|
||||
@@ -220,7 +221,6 @@ require (
|
||||
go.uber.org/zap/exp v0.3.0 // indirect
|
||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||
golang.org/x/crypto v0.49.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect
|
||||
golang.org/x/mod v0.34.0 // indirect
|
||||
golang.org/x/oauth2 v0.34.0 // indirect
|
||||
|
||||
@@ -18,16 +18,23 @@ type Device interface {
|
||||
}
|
||||
|
||||
type DeviceOptions struct {
|
||||
Context context.Context
|
||||
Logger logger.ContextLogger
|
||||
Handler tun.Handler
|
||||
UDPTimeout time.Duration
|
||||
CreateDialer func(interfaceName string) N.Dialer
|
||||
Name string
|
||||
MTU uint32
|
||||
Address []netip.Prefix
|
||||
Context context.Context
|
||||
Logger logger.ContextLogger
|
||||
System bool
|
||||
UDPTimeout time.Duration
|
||||
CreateDialer func(interfaceName string) N.Dialer
|
||||
Name string
|
||||
MTU uint32
|
||||
Address []netip.Prefix
|
||||
AllowedAddress []netip.Prefix
|
||||
}
|
||||
|
||||
func NewDevice(options DeviceOptions) (Device, error) {
|
||||
return newStackDevice(options)
|
||||
if !options.System {
|
||||
return newStackDevice(options)
|
||||
} else if !tun.WithGVisor {
|
||||
return newSystemDevice(options)
|
||||
} else {
|
||||
return newSystemStackDevice(options)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/gvisor/pkg/buffer"
|
||||
"github.com/sagernet/gvisor/pkg/tcpip"
|
||||
@@ -15,9 +16,6 @@ import (
|
||||
"github.com/sagernet/gvisor/pkg/tcpip/network/ipv4"
|
||||
"github.com/sagernet/gvisor/pkg/tcpip/network/ipv6"
|
||||
"github.com/sagernet/gvisor/pkg/tcpip/stack"
|
||||
"github.com/sagernet/gvisor/pkg/tcpip/transport/icmp"
|
||||
"github.com/sagernet/gvisor/pkg/tcpip/transport/tcp"
|
||||
"github.com/sagernet/gvisor/pkg/tcpip/transport/udp"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/transport/wireguard"
|
||||
"github.com/sagernet/sing-tun"
|
||||
@@ -29,15 +27,15 @@ import (
|
||||
)
|
||||
|
||||
type stackDevice struct {
|
||||
ctx context.Context
|
||||
logger log.ContextLogger
|
||||
stack *stack.Stack
|
||||
mtu uint32
|
||||
events chan wgTun.Event
|
||||
wgTun.Device
|
||||
ctx context.Context
|
||||
logger log.ContextLogger
|
||||
stack *stack.Stack
|
||||
mtu uint32
|
||||
events chan wgTun.Event
|
||||
outbound chan *stack.PacketBuffer
|
||||
packetOutbound chan *buf.Buffer
|
||||
done chan struct{}
|
||||
closeOnce sync.Once
|
||||
dispatcher stack.NetworkDispatcher
|
||||
inet4Address netip.Addr
|
||||
inet6Address netip.Addr
|
||||
@@ -84,13 +82,6 @@ func newStackDevice(options DeviceOptions) (*stackDevice, error) {
|
||||
}
|
||||
}
|
||||
tunDevice.stack = ipStack
|
||||
if options.Handler != nil {
|
||||
ipStack.SetTransportProtocolHandler(tcp.ProtocolNumber, tun.NewTCPForwarder(options.Context, ipStack, options.Handler).HandlePacket)
|
||||
ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, tun.NewUDPForwarder(options.Context, ipStack, options.Handler, options.UDPTimeout).HandlePacket)
|
||||
icmpForwarder := tun.NewICMPForwarder(options.Context, ipStack, options.Handler, options.UDPTimeout)
|
||||
ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber4, icmpForwarder.HandlePacket)
|
||||
ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber6, icmpForwarder.HandlePacket)
|
||||
}
|
||||
return tunDevice, nil
|
||||
}
|
||||
|
||||
@@ -141,11 +132,17 @@ func (w *stackDevice) ListenPacket(ctx context.Context, destination M.Socksaddr)
|
||||
}
|
||||
var networkProtocol tcpip.NetworkProtocolNumber
|
||||
if destination.IsIPv4() {
|
||||
if !w.inet4Address.IsValid() {
|
||||
return nil, E.New("missing IPv4 local address")
|
||||
}
|
||||
networkProtocol = header.IPv4ProtocolNumber
|
||||
bind.Addr = tun.AddressFromAddr(w.inet4Address)
|
||||
} else {
|
||||
if !w.inet6Address.IsValid() {
|
||||
return nil, E.New("missing IPv6 local address")
|
||||
}
|
||||
networkProtocol = header.IPv6ProtocolNumber
|
||||
bind.Addr = tun.AddressFromAddr(w.inet4Address)
|
||||
bind.Addr = tun.AddressFromAddr(w.inet6Address)
|
||||
}
|
||||
udpConn, err := gonet.DialUDP(w.stack, &bind, nil, networkProtocol)
|
||||
if err != nil {
|
||||
@@ -228,13 +225,15 @@ func (w *stackDevice) Events() <-chan wgTun.Event {
|
||||
}
|
||||
|
||||
func (w *stackDevice) Close() error {
|
||||
close(w.done)
|
||||
close(w.events)
|
||||
w.stack.Close()
|
||||
for _, endpoint := range w.stack.CleanupEndpoints() {
|
||||
endpoint.Abort()
|
||||
}
|
||||
w.stack.Wait()
|
||||
w.closeOnce.Do(func() {
|
||||
close(w.done)
|
||||
close(w.events)
|
||||
w.stack.Close()
|
||||
for _, endpoint := range w.stack.CleanupEndpoints() {
|
||||
endpoint.Abort()
|
||||
}
|
||||
w.stack.Wait()
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
191
transport/masque/device_system.go
Normal file
191
transport/masque/device_system.go
Normal file
@@ -0,0 +1,191 @@
|
||||
package masque
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-tun"
|
||||
"github.com/sagernet/sing/common"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/service"
|
||||
wgTun "github.com/sagernet/wireguard-go/tun"
|
||||
)
|
||||
|
||||
var _ Device = (*systemDevice)(nil)
|
||||
|
||||
type systemDevice struct {
|
||||
options DeviceOptions
|
||||
dialer N.Dialer
|
||||
device tun.Tun
|
||||
batchDevice tun.LinuxTUN
|
||||
events chan wgTun.Event
|
||||
closeOnce sync.Once
|
||||
inet4Address netip.Addr
|
||||
inet6Address netip.Addr
|
||||
}
|
||||
|
||||
func newSystemDevice(options DeviceOptions) (*systemDevice, error) {
|
||||
if options.Name == "" {
|
||||
options.Name = tun.CalculateInterfaceName("masque")
|
||||
}
|
||||
var inet4Address netip.Addr
|
||||
var inet6Address netip.Addr
|
||||
if len(options.Address) > 0 {
|
||||
if prefix := common.Find(options.Address, func(it netip.Prefix) bool {
|
||||
return it.Addr().Is4()
|
||||
}); prefix.IsValid() {
|
||||
inet4Address = prefix.Addr()
|
||||
}
|
||||
}
|
||||
if len(options.Address) > 0 {
|
||||
if prefix := common.Find(options.Address, func(it netip.Prefix) bool {
|
||||
return it.Addr().Is6()
|
||||
}); prefix.IsValid() {
|
||||
inet6Address = prefix.Addr()
|
||||
}
|
||||
}
|
||||
return &systemDevice{
|
||||
options: options,
|
||||
dialer: options.CreateDialer(options.Name),
|
||||
events: make(chan wgTun.Event, 1),
|
||||
inet4Address: inet4Address,
|
||||
inet6Address: inet6Address,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (w *systemDevice) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
return w.dialer.DialContext(ctx, network, destination)
|
||||
}
|
||||
|
||||
func (w *systemDevice) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||
return w.dialer.ListenPacket(ctx, destination)
|
||||
}
|
||||
|
||||
func (w *systemDevice) Inet4Address() netip.Addr {
|
||||
return w.inet4Address
|
||||
}
|
||||
|
||||
func (w *systemDevice) Inet6Address() netip.Addr {
|
||||
return w.inet6Address
|
||||
}
|
||||
|
||||
func (w *systemDevice) Start() error {
|
||||
networkManager := service.FromContext[adapter.NetworkManager](w.options.Context)
|
||||
tunOptions := tun.Options{
|
||||
Name: w.options.Name,
|
||||
Inet4Address: common.Filter(w.options.Address, func(it netip.Prefix) bool {
|
||||
return it.Addr().Is4()
|
||||
}),
|
||||
Inet6Address: common.Filter(w.options.Address, func(it netip.Prefix) bool {
|
||||
return it.Addr().Is6()
|
||||
}),
|
||||
MTU: w.options.MTU,
|
||||
GSO: true,
|
||||
InterfaceScope: true,
|
||||
Inet4RouteAddress: common.Filter(w.options.AllowedAddress, func(it netip.Prefix) bool {
|
||||
return it.Addr().Is4()
|
||||
}),
|
||||
Inet6RouteAddress: common.Filter(w.options.AllowedAddress, func(it netip.Prefix) bool {
|
||||
return it.Addr().Is6()
|
||||
}),
|
||||
InterfaceMonitor: networkManager.InterfaceMonitor(),
|
||||
InterfaceFinder: networkManager.InterfaceFinder(),
|
||||
Logger: w.options.Logger,
|
||||
}
|
||||
if runtime.GOOS == "darwin" {
|
||||
tunOptions.AutoRoute = true
|
||||
}
|
||||
tunInterface, err := tun.New(tunOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = tunInterface.Start()
|
||||
if err != nil {
|
||||
tunInterface.Close()
|
||||
return err
|
||||
}
|
||||
w.options.Logger.Notice("started at ", w.options.Name)
|
||||
w.device = tunInterface
|
||||
batchTUN, isBatchTUN := tunInterface.(tun.LinuxTUN)
|
||||
if isBatchTUN && batchTUN.BatchSize() > 1 {
|
||||
w.batchDevice = batchTUN
|
||||
}
|
||||
w.events <- wgTun.EventUp
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *systemDevice) File() *os.File {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *systemDevice) Read(bufs [][]byte, sizes []int, offset int) (count int, err error) {
|
||||
if w.batchDevice != nil {
|
||||
count, err = w.batchDevice.BatchRead(bufs, offset-tun.PacketOffset, sizes)
|
||||
} else {
|
||||
sizes[0], err = w.device.Read(bufs[0][offset-tun.PacketOffset:])
|
||||
if err == nil {
|
||||
count = 1
|
||||
} else if errors.Is(err, tun.ErrTooManySegments) {
|
||||
err = wgTun.ErrTooManySegments
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (w *systemDevice) Write(bufs [][]byte, offset int) (count int, err error) {
|
||||
if w.batchDevice != nil {
|
||||
return w.batchDevice.BatchWrite(bufs, offset)
|
||||
}
|
||||
for _, packet := range bufs {
|
||||
if tun.PacketOffset > 0 {
|
||||
clear(packet[offset-tun.PacketOffset : offset])
|
||||
tun.PacketFillHeader(packet[offset-tun.PacketOffset:], tun.PacketIPVersion(packet[offset:]))
|
||||
}
|
||||
_, err = w.device.Write(packet[offset-tun.PacketOffset:])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (w *systemDevice) Flush() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *systemDevice) MTU() (int, error) {
|
||||
return int(w.options.MTU), nil
|
||||
}
|
||||
|
||||
func (w *systemDevice) Name() (string, error) {
|
||||
return w.options.Name, nil
|
||||
}
|
||||
|
||||
func (w *systemDevice) Events() <-chan wgTun.Event {
|
||||
return w.events
|
||||
}
|
||||
|
||||
func (w *systemDevice) Close() error {
|
||||
var err error
|
||||
w.closeOnce.Do(func() {
|
||||
close(w.events)
|
||||
if w.device != nil {
|
||||
err = w.device.Close()
|
||||
}
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (w *systemDevice) BatchSize() int {
|
||||
if w.batchDevice != nil {
|
||||
return w.batchDevice.BatchSize()
|
||||
}
|
||||
return 1
|
||||
}
|
||||
200
transport/masque/device_system_stack.go
Normal file
200
transport/masque/device_system_stack.go
Normal file
@@ -0,0 +1,200 @@
|
||||
//go:build with_gvisor
|
||||
|
||||
package masque
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/gvisor/pkg/buffer"
|
||||
"github.com/sagernet/gvisor/pkg/tcpip"
|
||||
"github.com/sagernet/gvisor/pkg/tcpip/header"
|
||||
"github.com/sagernet/gvisor/pkg/tcpip/network/ipv4"
|
||||
"github.com/sagernet/gvisor/pkg/tcpip/network/ipv6"
|
||||
"github.com/sagernet/gvisor/pkg/tcpip/stack"
|
||||
"github.com/sagernet/sing-tun"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
var _ Device = (*systemStackDevice)(nil)
|
||||
|
||||
type systemStackDevice struct {
|
||||
*systemDevice
|
||||
stack *stack.Stack
|
||||
endpoint *systemStackEndpoint
|
||||
writeBufs [][]byte
|
||||
closeOnce sync.Once
|
||||
}
|
||||
|
||||
func newSystemStackDevice(options DeviceOptions) (*systemStackDevice, error) {
|
||||
system, err := newSystemDevice(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
endpoint := &systemStackEndpoint{
|
||||
mtu: options.MTU,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
ipStack, err := tun.NewGVisorStackWithOptions(endpoint, stack.NICOptions{}, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, prefix := range options.Address {
|
||||
addr := tun.AddressFromAddr(prefix.Addr())
|
||||
protoAddr := tcpip.ProtocolAddress{
|
||||
AddressWithPrefix: tcpip.AddressWithPrefix{
|
||||
Address: addr,
|
||||
PrefixLen: prefix.Bits(),
|
||||
},
|
||||
}
|
||||
if prefix.Addr().Is4() {
|
||||
protoAddr.Protocol = ipv4.ProtocolNumber
|
||||
} else {
|
||||
protoAddr.Protocol = ipv6.ProtocolNumber
|
||||
}
|
||||
gErr := ipStack.AddProtocolAddress(tun.DefaultNIC, protoAddr, stack.AddressProperties{})
|
||||
if gErr != nil {
|
||||
return nil, E.New("parse local address ", protoAddr.AddressWithPrefix, ": ", gErr.String())
|
||||
}
|
||||
}
|
||||
sd := &systemStackDevice{
|
||||
systemDevice: system,
|
||||
stack: ipStack,
|
||||
endpoint: endpoint,
|
||||
}
|
||||
endpoint.device = sd
|
||||
return sd, nil
|
||||
}
|
||||
|
||||
func (w *systemStackDevice) Write(bufs [][]byte, offset int) (count int, err error) {
|
||||
if w.batchDevice != nil {
|
||||
w.writeBufs = w.writeBufs[:0]
|
||||
for _, packet := range bufs {
|
||||
if !w.writeStack(packet[offset:]) {
|
||||
w.writeBufs = append(w.writeBufs, packet)
|
||||
}
|
||||
}
|
||||
if len(w.writeBufs) > 0 {
|
||||
return w.batchDevice.BatchWrite(w.writeBufs, offset)
|
||||
}
|
||||
} else {
|
||||
for _, packet := range bufs {
|
||||
if !w.writeStack(packet[offset:]) {
|
||||
if tun.PacketOffset > 0 {
|
||||
clear(packet[offset-tun.PacketOffset : offset])
|
||||
tun.PacketFillHeader(packet[offset-tun.PacketOffset:], tun.PacketIPVersion(packet[offset:]))
|
||||
}
|
||||
_, err = w.device.Write(packet[offset-tun.PacketOffset:])
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (w *systemStackDevice) Close() error {
|
||||
var err error
|
||||
w.closeOnce.Do(func() {
|
||||
close(w.endpoint.done)
|
||||
w.stack.Close()
|
||||
for _, endpoint := range w.stack.CleanupEndpoints() {
|
||||
endpoint.Abort()
|
||||
}
|
||||
w.stack.Wait()
|
||||
err = w.systemDevice.Close()
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (w *systemStackDevice) writeStack(packet []byte) bool {
|
||||
var (
|
||||
networkProtocol tcpip.NetworkProtocolNumber
|
||||
destination netip.Addr
|
||||
)
|
||||
switch header.IPVersion(packet) {
|
||||
case header.IPv4Version:
|
||||
networkProtocol = header.IPv4ProtocolNumber
|
||||
destination = netip.AddrFrom4(header.IPv4(packet).DestinationAddress().As4())
|
||||
case header.IPv6Version:
|
||||
networkProtocol = header.IPv6ProtocolNumber
|
||||
destination = netip.AddrFrom16(header.IPv6(packet).DestinationAddress().As16())
|
||||
}
|
||||
for _, prefix := range w.options.Address {
|
||||
if prefix.Contains(destination) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
packetBuffer := stack.NewPacketBuffer(stack.PacketBufferOptions{
|
||||
Payload: buffer.MakeWithData(packet),
|
||||
})
|
||||
w.endpoint.dispatcher.DeliverNetworkPacket(networkProtocol, packetBuffer)
|
||||
packetBuffer.DecRef()
|
||||
return true
|
||||
}
|
||||
|
||||
type systemStackEndpoint struct {
|
||||
mtu uint32
|
||||
done chan struct{}
|
||||
device Device
|
||||
dispatcher stack.NetworkDispatcher
|
||||
}
|
||||
|
||||
func (ep *systemStackEndpoint) MTU() uint32 {
|
||||
return ep.mtu
|
||||
}
|
||||
|
||||
func (ep *systemStackEndpoint) SetMTU(mtu uint32) {
|
||||
}
|
||||
|
||||
func (ep *systemStackEndpoint) MaxHeaderLength() uint16 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (ep *systemStackEndpoint) LinkAddress() tcpip.LinkAddress {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (ep *systemStackEndpoint) SetLinkAddress(addr tcpip.LinkAddress) {
|
||||
}
|
||||
|
||||
func (ep *systemStackEndpoint) Capabilities() stack.LinkEndpointCapabilities {
|
||||
return stack.CapabilityRXChecksumOffload
|
||||
}
|
||||
|
||||
func (ep *systemStackEndpoint) Attach(dispatcher stack.NetworkDispatcher) {
|
||||
ep.dispatcher = dispatcher
|
||||
}
|
||||
|
||||
func (ep *systemStackEndpoint) IsAttached() bool {
|
||||
return ep.dispatcher != nil
|
||||
}
|
||||
|
||||
func (ep *systemStackEndpoint) Wait() {
|
||||
}
|
||||
|
||||
func (ep *systemStackEndpoint) ARPHardwareType() header.ARPHardwareType {
|
||||
return header.ARPHardwareNone
|
||||
}
|
||||
|
||||
func (ep *systemStackEndpoint) AddHeader(buffer *stack.PacketBuffer) {
|
||||
}
|
||||
|
||||
func (ep *systemStackEndpoint) ParseHeader(ptr *stack.PacketBuffer) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (ep *systemStackEndpoint) WritePackets(list stack.PacketBufferList) (int, tcpip.Error) {
|
||||
for _, packetBuffer := range list.AsSlice() {
|
||||
packet := packetBuffer.ToView().AsSlice()
|
||||
ep.device.Write([][]byte{packet}, 0)
|
||||
}
|
||||
return list.Len(), nil
|
||||
}
|
||||
|
||||
func (ep *systemStackEndpoint) Close() {
|
||||
}
|
||||
|
||||
func (ep *systemStackEndpoint) SetOnCloseAction(f func()) {
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user