保存本机重置通道初稿并建立接入前检查点

This commit is contained in:
xuans 2026-09-21 00:27:42 +08:00
parent 856539d105
commit ec800fc4b6
8 changed files with 1045 additions and 0 deletions

View File

@ -24,6 +24,7 @@ require (
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
filippo.io/edwards25519 v1.1.1 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
github.com/alibabacloud-go/debug v1.0.1 // indirect
github.com/alibabacloud-go/tea-utils/v2 v2.0.9 // indirect

View File

@ -8,6 +8,8 @@ cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCB
filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw=
filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6 h1:eIf+iGJxdU4U9ypaUfbtOWCsZSbTb8AUHvyPrxu6mAA=
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6/go.mod h1:4EUIoxs/do24zMOGGqYVWgw0s9NtiylnJglOeEB5UJo=
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc=

View File

@ -0,0 +1,118 @@
package localreset
import (
"bufio"
"context"
"net"
"sync"
"time"
)
type Client struct {
conn net.Conn
reader *bufio.Reader
nonce string
methods []Method
stage int
stop func() bool
mu sync.Mutex
once sync.Once
}
// Begin 只接受已通过 Dial 校验身份的连接;调用过程不会通过参数或文件传递密码。
func Begin(ctx context.Context, conn net.Conn) (*Client, error) {
if conn == nil {
return nil, ErrUnavailable
}
deadline := time.Now().Add(SessionTimeout)
if requested, ok := ctx.Deadline(); ok && requested.Before(deadline) {
deadline = requested
}
if err := conn.SetDeadline(deadline); err != nil {
_ = conn.Close()
return nil, err
}
client := &Client{conn: conn, reader: bufio.NewReaderSize(conn, MaxFrameSize+1)}
client.stop = context.AfterFunc(ctx, func() { _ = conn.Close() })
var response Response
if err := readFrame(client.reader, &response); err != nil || !validChallenge(response) ||
len(response.Methods) == 0 || len(response.Methods) > 8 {
_ = client.Close()
return nil, ErrProtocol
}
client.nonce = response.Nonce
client.methods = response.Methods
return client, nil
}
func (c *Client) Methods() []Method { return append([]Method(nil), c.methods...) }
func (c *Client) Select(method string) error {
return c.exchange(Request{Action: "select", Method: method}, 0)
}
func (c *Client) Verify(code string) error {
return c.exchange(Request{Action: "verify", Code: code}, 1)
}
func (c *Client) Reset(password string) error {
return c.exchange(Request{Action: "reset", Password: password}, 2)
}
func (c *Client) exchange(request Request, stage int) error {
c.mu.Lock()
defer c.mu.Unlock()
if c.stage != stage {
_ = c.Close()
return ErrProtocol
}
request.Version, request.Nonce = Version, c.nonce
if !validRequest(request, c.nonce, request.Action) || writeFrame(c.conn, request) != nil {
c.stage = -1
_ = c.Close()
return ErrProtocol
}
var response Response
if err := readFrame(c.reader, &response); err != nil {
c.stage = -1
_ = c.Close()
return ErrProtocol
}
if stage == 2 {
c.stage = -1
_ = c.Close()
if response.Version != Version || !response.OK || response.Code != "COMPLETE" ||
response.Nonce != "" || len(response.Methods) != 0 {
return ErrProtocol
}
return nil
}
if !validChallenge(response) || response.Nonce == c.nonce || len(response.Methods) != 0 {
c.stage = -1
_ = c.Close()
return ErrProtocol
}
c.nonce = response.Nonce
c.stage++
return nil
}
func validChallenge(response Response) bool {
if response.Version != Version || !response.OK || response.Code != "CHALLENGE" || len(response.Nonce) != 64 {
return false
}
for _, value := range response.Nonce {
if (value < '0' || value > '9') && (value < 'a' || value > 'f') {
return false
}
}
return true
}
func (c *Client) Close() error {
var err error
c.once.Do(func() {
if c.stop != nil {
c.stop()
}
err = c.conn.Close()
})
return err
}

View File

@ -0,0 +1,315 @@
package localreset
import (
"bufio"
"bytes"
"context"
"crypto/rand"
"crypto/subtle"
"encoding/hex"
"encoding/json"
"errors"
"io"
"net"
"sync"
"time"
)
const (
Version = 1
MaxFrameSize = 16 << 10
SessionTimeout = 2 * time.Minute
MaxConnections = 8
)
var (
ErrUnauthorized = errors.New("本机调用者身份不受信任")
ErrProtocol = errors.New("本机重置请求无效或已失效")
ErrUnavailable = errors.New("本机重置通道不可用")
)
type Peer struct {
Identity string
}
type Method struct {
ID string `json:"id"`
Label string `json:"label"`
}
type Request struct {
Version int `json:"version"`
Nonce string `json:"nonce"`
Action string `json:"action"`
Method string `json:"method,omitempty"`
Code string `json:"code,omitempty"`
Password string `json:"password,omitempty"`
}
type Response struct {
Version int `json:"version"`
OK bool `json:"ok"`
Code string `json:"code"`
Nonce string `json:"nonce,omitempty"`
Methods []Method `json:"methods,omitempty"`
}
// Session 由主服务创建,身份验证及密码持久化仍由主服务负责。
type Session interface {
Methods() []Method
Select(context.Context, string) error
Verify(context.Context, string) error
Reset(context.Context, string) error
Close()
}
type Factory func(context.Context, Peer) (Session, error)
type failureWindow struct {
since time.Time
count int
}
type Server struct {
listener net.Listener
authorize func(net.Conn) (Peer, error)
cleanup func() error
mu sync.Mutex
active map[net.Conn]struct{}
failures map[string]failureWindow
closed bool
serving bool
timeout time.Duration
now func() time.Time
once sync.Once
closeErr error
workers sync.WaitGroup
}
func newServer(listener net.Listener, authorize func(net.Conn) (Peer, error), cleanup func() error) *Server {
return &Server{listener: listener, authorize: authorize, cleanup: cleanup,
active: make(map[net.Conn]struct{}), failures: make(map[string]failureWindow),
timeout: SessionTimeout, now: time.Now}
}
// Serve 限制并发连接,取消时关闭全部连接并等待业务处理退出。
func (s *Server) Serve(ctx context.Context, factory Factory) error {
if s == nil || factory == nil {
return ErrUnavailable
}
s.mu.Lock()
if s.closed || s.serving {
s.mu.Unlock()
return ErrUnavailable
}
s.serving = true
s.mu.Unlock()
stop := context.AfterFunc(ctx, func() { _ = s.Close() })
defer stop()
defer s.workers.Wait()
defer s.Close()
for {
conn, err := s.listener.Accept()
if err != nil {
if ctx.Err() != nil || errors.Is(err, net.ErrClosed) {
return nil
}
return err
}
s.mu.Lock()
if s.closed || len(s.active) >= MaxConnections {
s.mu.Unlock()
_ = conn.Close()
continue
}
s.active[conn] = struct{}{}
s.workers.Add(1)
s.mu.Unlock()
go func() {
defer s.workers.Done()
defer conn.Close()
defer func() { s.mu.Lock(); delete(s.active, conn); s.mu.Unlock() }()
s.handle(ctx, conn, factory)
}()
}
}
func (s *Server) Close() error {
if s == nil {
return nil
}
s.once.Do(func() {
s.mu.Lock()
s.closed = true
s.closeErr = s.listener.Close()
for conn := range s.active {
_ = conn.Close()
}
s.mu.Unlock()
if s.cleanup != nil {
s.closeErr = errors.Join(s.closeErr, s.cleanup())
}
})
return s.closeErr
}
func (s *Server) permit(identity string) bool {
s.mu.Lock()
defer s.mu.Unlock()
now := s.now()
for key, entry := range s.failures {
if !now.Before(entry.since.Add(time.Minute)) {
delete(s.failures, key)
}
}
entry, found := s.failures[identity]
return (!found && len(s.failures) < 128) || (found && entry.count < 5)
}
func (s *Server) failed(identity string) {
s.mu.Lock()
defer s.mu.Unlock()
entry := s.failures[identity]
if entry.since.IsZero() {
entry.since = s.now()
}
entry.count++
s.failures[identity] = entry
}
func (s *Server) handle(parent context.Context, conn net.Conn, factory Factory) {
ctx, cancel := context.WithTimeout(parent, s.timeout)
defer cancel()
deadline, _ := ctx.Deadline()
if conn.SetDeadline(deadline) != nil {
return
}
peer, err := s.authorize(conn)
if err != nil {
_ = writeFrame(conn, Response{Version: Version, Code: "UNAUTHORIZED"})
return
}
if !s.permit(peer.Identity) {
_ = writeFrame(conn, Response{Version: Version, Code: "RATE_LIMITED"})
return
}
session, err := factory(ctx, peer)
if err != nil || session == nil {
if session != nil {
session.Close()
}
_ = writeFrame(conn, Response{Version: Version, Code: "UNAVAILABLE"})
return
}
defer session.Close()
methods := session.Methods()
if len(methods) == 0 || len(methods) > 8 {
_ = writeFrame(conn, Response{Version: Version, Code: "UNAVAILABLE"})
return
}
reader := bufio.NewReaderSize(conn, MaxFrameSize+1)
for index, action := range []string{"select", "verify", "reset"} {
nonce, err := newNonce()
if err != nil {
return
}
response := Response{Version: Version, OK: true, Code: "CHALLENGE", Nonce: nonce}
if index == 0 {
response.Methods = methods
}
if writeFrame(conn, response) != nil {
return
}
var request Request
err = readFrame(reader, &request)
if err != nil || ctx.Err() != nil || !validRequest(request, nonce, action) {
s.failed(peer.Identity)
_ = writeFrame(conn, Response{Version: Version, Code: "INVALID_REQUEST"})
return
}
switch action {
case "select":
allowed := false
for _, method := range methods {
if method.ID == request.Method {
allowed = true
}
}
if !allowed {
err = ErrProtocol
} else {
err = session.Select(ctx, request.Method)
}
case "verify":
err = session.Verify(ctx, request.Code)
case "reset":
err = session.Reset(ctx, request.Password)
}
if err != nil {
s.failed(peer.Identity)
_ = writeFrame(conn, Response{Version: Version, Code: "REJECTED"})
return
}
}
_ = writeFrame(conn, Response{Version: Version, OK: true, Code: "COMPLETE"})
}
func validRequest(request Request, nonce, action string) bool {
if request.Version != Version || request.Action != action ||
subtle.ConstantTimeCompare([]byte(request.Nonce), []byte(nonce)) != 1 {
return false
}
switch action {
case "select":
return request.Method != "" && len(request.Method) <= 128 && request.Code == "" && request.Password == ""
case "verify":
return request.Code != "" && len(request.Code) <= 128 && request.Method == "" && request.Password == ""
case "reset":
return request.Password != "" && len(request.Password) <= 256 && request.Method == "" && request.Code == ""
}
return false
}
func newNonce() (string, error) {
var value [32]byte
if _, err := rand.Read(value[:]); err != nil {
return "", err
}
return hex.EncodeToString(value[:]), nil
}
func readFrame(reader *bufio.Reader, value any) error {
data, err := reader.ReadSlice('\n')
if err != nil || len(data) > MaxFrameSize {
return ErrProtocol
}
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields()
if decoder.Decode(value) != nil {
return ErrProtocol
}
var extra any
if !errors.Is(decoder.Decode(&extra), io.EOF) {
return ErrProtocol
}
return nil
}
func writeFrame(writer io.Writer, value any) error {
data, err := json.Marshal(value)
if err != nil || len(data)+1 > MaxFrameSize {
return ErrProtocol
}
data = append(data, '\n')
for len(data) > 0 {
n, err := writer.Write(data)
if err != nil {
return err
}
if n <= 0 {
return io.ErrShortWrite
}
data = data[n:]
}
return nil
}

View File

@ -0,0 +1,281 @@
package localreset
import (
"bufio"
"context"
"errors"
"io"
"net"
"strings"
"sync/atomic"
"testing"
"time"
)
type testSession struct {
selected atomic.Int32
verified atomic.Int32
resets atomic.Int32
closed atomic.Int32
}
func (s *testSession) Methods() []Method { return []Method{{ID: "recovery", Label: "恢复码"}} }
func (s *testSession) Select(ctx context.Context, method string) error {
if ctx.Err() != nil || method != "recovery" {
return ErrProtocol
}
s.selected.Add(1)
return nil
}
func (s *testSession) Verify(ctx context.Context, code string) error {
if ctx.Err() != nil || s.selected.Load() != 1 || code != "test-code" {
return ErrProtocol
}
s.verified.Add(1)
return nil
}
func (s *testSession) Reset(ctx context.Context, password string) error {
if ctx.Err() != nil || s.verified.Load() != 1 || password != "Example123!" {
return ErrProtocol
}
s.resets.Add(1)
return nil
}
func (s *testSession) Close() { s.closed.Add(1) }
func testConnection(t *testing.T, timeout time.Duration) (*Server, net.Conn, *testSession, <-chan struct{}) {
t.Helper()
serverConn, clientConn := net.Pipe()
server := newServer(nil, func(net.Conn) (Peer, error) { return Peer{Identity: "test"}, nil }, nil)
server.timeout = timeout
session := &testSession{}
done := make(chan struct{})
ctx, cancel := context.WithCancel(context.Background())
go func() {
defer close(done)
defer serverConn.Close()
server.handle(ctx, serverConn, func(context.Context, Peer) (Session, error) { return session, nil })
}()
t.Cleanup(func() {
cancel()
_ = clientConn.Close()
_ = serverConn.Close()
select {
case <-done:
case <-time.After(time.Second):
t.Error("connection did not stop")
}
})
return server, clientConn, session, done
}
func TestProtocolRoundTrip(t *testing.T) {
_, conn, session, done := testConnection(t, time.Second)
client, err := Begin(context.Background(), conn)
if err != nil {
t.Fatal(err)
}
defer client.Close()
methods := client.Methods()
methods[0].ID = "mutated"
if client.Methods()[0].ID != "recovery" {
t.Fatal("methods leaked mutable state")
}
if err := client.Select("recovery"); err != nil {
t.Fatal(err)
}
if err := client.Verify("test-code"); err != nil {
t.Fatal(err)
}
if err := client.Reset("Example123!"); err != nil {
t.Fatal(err)
}
if err := client.Reset("Example123!"); err == nil {
t.Fatal("replay accepted")
}
<-done
if session.resets.Load() != 1 || session.closed.Load() != 1 {
t.Fatal("session not consumed and closed once")
}
}
func TestProtocolRejectsInvalidFrames(t *testing.T) {
cases := map[string]func(string) string{
"invalid-json": func(string) string { return "{broken}\n" },
"oversized": func(string) string { return strings.Repeat("x", MaxFrameSize+1) + "\n" },
"wrong-version": func(n string) string {
return `{"version":9,"nonce":"` + n + `","action":"select","method":"recovery"}` + "\n"
},
"wrong-nonce": func(string) string {
return `{"version":1,"nonce":"wrong","action":"select","method":"recovery"}` + "\n"
},
"skip-verification": func(n string) string {
return `{"version":1,"nonce":"` + n + `","action":"reset","password":"Example123!"}` + "\n"
},
"unknown-field": func(n string) string {
return `{"version":1,"nonce":"` + n + `","action":"select","method":"recovery","extra":true}` + "\n"
},
"trailing-object": func(n string) string {
return `{"version":1,"nonce":"` + n + `","action":"select","method":"recovery"}{}` + "\n"
},
"wrong-method": func(n string) string {
return `{"version":1,"nonce":"` + n + `","action":"select","method":"missing"}` + "\n"
},
"mixed-payload": func(n string) string {
return `{"version":1,"nonce":"` + n + `","action":"select","method":"recovery","password":"Example123!"}` + "\n"
},
}
for name, payload := range cases {
t.Run(name, func(t *testing.T) {
_, conn, session, done := testConnection(t, time.Second)
reader := bufio.NewReaderSize(conn, MaxFrameSize+1)
var challenge Response
if err := readFrame(reader, &challenge); err != nil {
t.Fatal(err)
}
go func() { _, _ = io.WriteString(conn, payload(challenge.Nonce)) }()
var response Response
if err := readFrame(reader, &response); err != nil || response.OK {
t.Fatalf("accepted malformed request: %+v %v", response, err)
}
<-done
if session.resets.Load() != 0 || session.closed.Load() != 1 {
t.Fatal("invalid request reached reset or leaked session")
}
})
}
}
func TestProtocolRotatesNonce(t *testing.T) {
_, conn, session, done := testConnection(t, time.Second)
reader := bufio.NewReaderSize(conn, MaxFrameSize+1)
var first, second, failure Response
if err := readFrame(reader, &first); err != nil {
t.Fatal(err)
}
if err := writeFrame(conn, Request{Version: Version, Nonce: first.Nonce, Action: "select", Method: "recovery"}); err != nil {
t.Fatal(err)
}
if err := readFrame(reader, &second); err != nil || second.Nonce == first.Nonce {
t.Fatal("nonce not rotated")
}
if err := writeFrame(conn, Request{Version: Version, Nonce: first.Nonce, Action: "verify", Code: "test-code"}); err != nil {
t.Fatal(err)
}
if err := readFrame(reader, &failure); err != nil || failure.OK {
t.Fatal("replayed nonce accepted")
}
<-done
if session.verified.Load() != 0 {
t.Fatal("replay reached verification")
}
}
func TestProtocolDeadlineClosesIdleConnection(t *testing.T) {
_, conn, session, done := testConnection(t, 30*time.Millisecond)
var response Response
if err := readFrame(bufio.NewReaderSize(conn, MaxFrameSize+1), &response); err != nil {
t.Fatal(err)
}
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("idle connection survived deadline")
}
if session.closed.Load() != 1 {
t.Fatal("expired session leaked")
}
}
func TestProtocolVerificationFailureIsTerminal(t *testing.T) {
_, conn, session, done := testConnection(t, time.Second)
client, err := Begin(context.Background(), conn)
if err != nil {
t.Fatal(err)
}
if err := client.Select("recovery"); err != nil {
t.Fatal(err)
}
if err := client.Verify("wrong-code"); err == nil {
t.Fatal("wrong code accepted")
}
if err := client.Reset("Example123!"); err == nil {
t.Fatal("failed session reused")
}
<-done
if session.resets.Load() != 0 {
t.Fatal("verification bypassed")
}
}
func TestFailureWindowExpires(t *testing.T) {
server := newServer(nil, nil, nil)
now := time.Now()
server.now = func() time.Time { return now }
for i := 0; i < 5; i++ {
if !server.permit("peer") {
t.Fatal("premature limit")
}
server.failed("peer")
}
if server.permit("peer") {
t.Fatal("failure limit bypassed")
}
now = now.Add(time.Minute)
if !server.permit("peer") {
t.Fatal("failure limit did not expire")
}
}
func TestClientCancellationClosesConnection(t *testing.T) {
_, conn, session, done := testConnection(t, time.Minute)
ctx, cancel := context.WithCancel(context.Background())
client, err := Begin(ctx, conn)
if err != nil {
t.Fatal(err)
}
defer client.Close()
cancel()
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("cancellation did not close connection")
}
if session.closed.Load() != 1 {
t.Fatal("cancelled session leaked")
}
}
func TestFactoryErrorDoesNotDiscloseDetails(t *testing.T) {
left, right := net.Pipe()
defer right.Close()
server := newServer(nil, func(net.Conn) (Peer, error) { return Peer{Identity: "test"}, nil }, nil)
go func() {
defer left.Close()
server.handle(context.Background(), left, func(context.Context, Peer) (Session, error) {
return nil, errors.New("secret detail")
})
}()
data, err := io.ReadAll(right)
if err != nil || strings.Contains(string(data), "secret") || !strings.Contains(string(data), "UNAVAILABLE") {
t.Fatalf("unsafe response: %s %v", data, err)
}
}
func TestUnauthorizedPeerCannotCreateSession(t *testing.T) {
left, right := net.Pipe()
defer right.Close()
server := newServer(nil, func(net.Conn) (Peer, error) { return Peer{}, ErrUnauthorized }, nil)
var calls atomic.Int32
go func() {
defer left.Close()
server.handle(context.Background(), left, func(context.Context, Peer) (Session, error) {
calls.Add(1)
return &testSession{}, nil
})
}()
data, err := io.ReadAll(right)
if err != nil || !strings.Contains(string(data), "UNAUTHORIZED") || calls.Load() != 0 {
t.Fatal("unauthorized peer reached factory")
}
}

View File

@ -0,0 +1,183 @@
//go:build linux
package localreset
import (
"context"
"errors"
"fmt"
"net"
"os"
"path/filepath"
"strconv"
"syscall"
"time"
"golang.org/x/sys/unix"
)
const DefaultPath = "/run/scert/admin-control.sock"
func socketPath(path string) (string, error) {
if path == "" {
path = DefaultPath
}
if !filepath.IsAbs(path) || filepath.Clean(path) != path || len(path) > 107 {
return "", ErrUnavailable
}
return path, nil
}
// validateDirectory 拒绝符号链接及不可信的可写上级目录。
func validateDirectory(parent string, owner uint32) error {
for path := parent; ; path = filepath.Dir(path) {
info, err := os.Lstat(path)
if err != nil || !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
return ErrUnauthorized
}
stat, ok := info.Sys().(*syscall.Stat_t)
if !ok || (stat.Uid != owner && stat.Uid != 0) {
return ErrUnauthorized
}
if path == parent {
if stat.Uid != owner || info.Mode().Perm()&0022 != 0 {
return ErrUnauthorized
}
} else if info.Mode().Perm()&0022 != 0 && info.Mode()&os.ModeSticky == 0 {
return ErrUnauthorized
}
if path == filepath.Dir(path) {
return nil
}
}
}
func validateSocket(path string, owner uint32) (os.FileInfo, error) {
info, err := os.Lstat(path)
if err != nil {
return nil, err
}
stat, ok := info.Sys().(*syscall.Stat_t)
if !ok || stat.Uid != owner || info.Mode()&os.ModeSocket == 0 || info.Mode().Perm()&0077 != 0 {
return nil, ErrUnauthorized
}
return info, nil
}
func Listen(path string) (*Server, error) {
path, err := socketPath(path)
if err != nil {
return nil, err
}
parent := filepath.Dir(path)
if err := os.Mkdir(parent, 0700); err != nil && !errors.Is(err, os.ErrExist) {
return nil, err
}
owner := uint32(os.Geteuid())
if err := validateDirectory(parent, owner); err != nil {
return nil, err
}
info, err := validateSocket(path, owner)
if err == nil {
conn, dialErr := net.DialTimeout("unix", path, 100*time.Millisecond)
if dialErr == nil {
_ = conn.Close()
return nil, ErrUnavailable
}
if !errors.Is(dialErr, syscall.ECONNREFUSED) {
return nil, ErrUnavailable
}
current, checkErr := validateSocket(path, owner)
if checkErr != nil || !os.SameFile(info, current) {
return nil, ErrUnauthorized
}
if err := os.Remove(path); err != nil {
return nil, err
}
} else if !errors.Is(err, os.ErrNotExist) {
return nil, err
}
listener, err := net.ListenUnix("unix", &net.UnixAddr{Name: path, Net: "unix"})
if err != nil {
return nil, err
}
listener.SetUnlinkOnClose(false)
info, err = os.Lstat(path)
if err != nil {
_ = listener.Close()
return nil, err
}
cleanup := func() error {
current, err := os.Lstat(path)
if errors.Is(err, os.ErrNotExist) {
return nil
}
if err != nil {
return err
}
if !os.SameFile(info, current) {
return ErrUnauthorized
}
return os.Remove(path)
}
if err := os.Chmod(path, 0600); err != nil {
_ = listener.Close()
_ = cleanup()
return nil, err
}
authorize := func(conn net.Conn) (Peer, error) {
uid, err := peerUID(conn)
if err != nil || (uid != 0 && uid != owner) {
return Peer{}, ErrUnauthorized
}
return Peer{Identity: "uid:" + strconv.FormatUint(uint64(uid), 10)}, nil
}
return newServer(listener, authorize, cleanup), nil
}
func peerUID(conn net.Conn) (uint32, error) {
socket, ok := conn.(*net.UnixConn)
if !ok {
return 0, ErrUnauthorized
}
raw, err := socket.SyscallConn()
if err != nil {
return 0, err
}
var credential *unix.Ucred
var credentialErr error
err = raw.Control(func(fd uintptr) {
credential, credentialErr = unix.GetsockoptUcred(int(fd), unix.SOL_SOCKET, unix.SO_PEERCRED)
})
if err != nil || credentialErr != nil || credential == nil || credential.Pid <= 0 {
return 0, ErrUnauthorized
}
return credential.Uid, nil
}
// Dial 同时验证目录、套接字和服务进程身份,不能只信任路径名称。
func Dial(ctx context.Context, path string, expectedUID uint32) (net.Conn, error) {
path, err := socketPath(path)
if err != nil {
return nil, err
}
if err := validateDirectory(filepath.Dir(path), expectedUID); err != nil {
return nil, err
}
before, err := validateSocket(path, expectedUID)
if err != nil {
return nil, err
}
dialer := net.Dialer{}
conn, err := dialer.DialContext(ctx, "unix", path)
if err != nil {
return nil, err
}
uid, err := peerUID(conn)
after, checkErr := validateSocket(path, expectedUID)
if err != nil || uid != expectedUID || checkErr != nil || !os.SameFile(before, after) {
_ = conn.Close()
return nil, fmt.Errorf("%w", ErrUnauthorized)
}
return conn, nil
}

View File

@ -0,0 +1,13 @@
//go:build !linux && !windows
package localreset
import (
"context"
"net"
)
const DefaultPath = ""
func Listen(string) (*Server, error) { return nil, ErrUnavailable }
func Dial(context.Context, string, uint32) (net.Conn, error) { return nil, ErrUnavailable }

View File

@ -0,0 +1,132 @@
//go:build windows
package localreset
import (
"context"
"net"
"strings"
"github.com/Microsoft/go-winio"
"golang.org/x/sys/windows"
)
const DefaultPath = `\\.\pipe\scert-admin-control`
const pipeSDDL = "D:P(D;;GA;;;NU)(A;;GA;;;SY)(A;;GA;;;BA)"
func pipePath(path string) (string, error) {
if path == "" {
path = DefaultPath
}
prefix := `\\.\pipe\`
if !strings.HasPrefix(path, prefix) || len(path) <= len(prefix) ||
strings.ContainsAny(strings.TrimPrefix(path, prefix), `\/:`) {
return "", ErrUnavailable
}
return path, nil
}
func Listen(path string) (*Server, error) {
return listenPipe(path, pipeSDDL)
}
func listenPipe(path, sddl string) (*Server, error) {
path, err := pipePath(path)
if err != nil {
return nil, err
}
listener, err := winio.ListenPipe(path, &winio.PipeConfig{
SecurityDescriptor: sddl,
InputBufferSize: MaxFrameSize,
OutputBufferSize: MaxFrameSize,
})
if err != nil {
return nil, err
}
return newServer(listener, func(conn net.Conn) (Peer, error) {
return pipePeer(conn, false)
}, nil), nil
}
// pipePeer 除管道 ACL 外再次验证进程令牌,拒绝非提升管理员及网络登录令牌。
func pipePeer(conn net.Conn, server bool) (Peer, error) {
provider, ok := conn.(interface{ Fd() uintptr })
if !ok || provider.Fd() == 0 {
return Peer{}, ErrUnauthorized
}
var pid uint32
var err error
if server {
err = windows.GetNamedPipeServerProcessId(windows.Handle(provider.Fd()), &pid)
} else {
err = windows.GetNamedPipeClientProcessId(windows.Handle(provider.Fd()), &pid)
}
if err != nil || pid == 0 {
return Peer{}, ErrUnauthorized
}
process, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid)
if err != nil {
return Peer{}, ErrUnauthorized
}
defer windows.CloseHandle(process)
var token windows.Token
if windows.OpenProcessToken(process, windows.TOKEN_QUERY, &token) != nil {
return Peer{}, ErrUnauthorized
}
defer token.Close()
return tokenPeer(token)
}
func tokenPeer(token windows.Token) (Peer, error) {
user, err := token.GetTokenUser()
if err != nil || user == nil || user.User.Sid == nil {
return Peer{}, ErrUnauthorized
}
groups, err := token.GetTokenGroups()
if err != nil {
return Peer{}, ErrUnauthorized
}
network, err := windows.CreateWellKnownSid(windows.WinNetworkSid)
if err != nil {
return Peer{}, ErrUnauthorized
}
admin, err := windows.CreateWellKnownSid(windows.WinBuiltinAdministratorsSid)
if err != nil {
return Peer{}, ErrUnauthorized
}
enabledAdmin := false
for _, group := range groups.AllGroups() {
if group.Sid.Equals(network) {
return Peer{}, ErrUnauthorized
}
if group.Sid.Equals(admin) && group.Attributes&windows.SE_GROUP_ENABLED != 0 &&
group.Attributes&windows.SE_GROUP_USE_FOR_DENY_ONLY == 0 {
enabledAdmin = true
}
}
system, err := windows.CreateWellKnownSid(windows.WinLocalSystemSid)
if err != nil {
return Peer{}, ErrUnauthorized
}
if !user.User.Sid.Equals(system) && (!enabledAdmin || !token.IsElevated()) {
return Peer{}, ErrUnauthorized
}
return Peer{Identity: "sid:" + user.User.Sid.String()}, nil
}
// expectedUID 仅用于 Unix;Windows 通过 SYSTEM 或提升管理员令牌识别服务。
func Dial(ctx context.Context, path string, expectedUID uint32) (net.Conn, error) {
path, err := pipePath(path)
if err != nil {
return nil, err
}
conn, err := winio.DialPipeContext(ctx, path)
if err != nil {
return nil, err
}
if _, err := pipePeer(conn, true); err != nil {
_ = conn.Close()
return nil, err
}
return conn, nil
}