diff --git a/common/sniff/ntp.go b/common/sniff/ntp.go new file mode 100644 index 00000000..8b844c5b --- /dev/null +++ b/common/sniff/ntp.go @@ -0,0 +1,58 @@ +package sniff + +import ( + "context" + "encoding/binary" + "os" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" +) + +func NTP(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error { + // NTP packets must be at least 48 bytes long (standard NTP header size). + pLen := len(packet) + if pLen < 48 { + return os.ErrInvalid + } + // Check the LI (Leap Indicator) and Version Number (VN) in the first byte. + // We'll primarily focus on ensuring the version is valid for NTP. + // Many NTP versions are used, but let's check for generally accepted ones (3 & 4 for IPv4, plus potential extensions/customizations) + firstByte := packet[0] + li := (firstByte >> 6) & 0x03 // Extract LI + vn := (firstByte >> 3) & 0x07 // Extract VN + mode := firstByte & 0x07 // Extract Mode + + // Leap Indicator should be a valid value (0-3). + if li > 3 { + return os.ErrInvalid + } + + // Version Check (common NTP versions are 3 and 4) + if vn != 3 && vn != 4 { + return os.ErrInvalid + } + + // Check the Mode field for a client request (Mode 3). This validates it *is* a request. + if mode != 3 { + return os.ErrInvalid + } + + // Check Root Delay and Root Dispersion. While not strictly *required* for a request, + // we can check if they appear to be reasonable values (not excessively large). + rootDelay := binary.BigEndian.Uint32(packet[4:8]) + rootDispersion := binary.BigEndian.Uint32(packet[8:12]) + + // Check for unreasonably large root delay and dispersion. NTP RFC specifies max values of approximately 16 seconds. + // Convert to milliseconds for easy comparison. Each unit is 1/2^16 seconds. + if float64(rootDelay)/65536.0 > 16.0 { + return os.ErrInvalid + } + if float64(rootDispersion)/65536.0 > 16.0 { + return os.ErrInvalid + } + + metadata.Protocol = C.ProtocolNTP + + return nil +} diff --git a/common/sniff/ntp_test.go b/common/sniff/ntp_test.go new file mode 100644 index 00000000..5da94785 --- /dev/null +++ b/common/sniff/ntp_test.go @@ -0,0 +1,33 @@ +package sniff_test + +import ( + "context" + "encoding/hex" + "os" + "testing" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/sniff" + C "github.com/sagernet/sing-box/constant" + + "github.com/stretchr/testify/require" +) + +func TestSniffNTP(t *testing.T) { + t.Parallel() + packet, err := hex.DecodeString("1b0006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") + require.NoError(t, err) + var metadata adapter.InboundContext + err = sniff.NTP(context.Background(), &metadata, packet) + require.NoError(t, err) + require.Equal(t, metadata.Protocol, C.ProtocolNTP) +} + +func TestSniffNTPFailed(t *testing.T) { + t.Parallel() + packet, err := hex.DecodeString("400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") + require.NoError(t, err) + var metadata adapter.InboundContext + err = sniff.NTP(context.Background(), &metadata, packet) + require.ErrorIs(t, err, os.ErrInvalid) +} diff --git a/docs/configuration/route/sniff.md b/docs/configuration/route/sniff.md index 5c8f218b..23067dfb 100644 --- a/docs/configuration/route/sniff.md +++ b/docs/configuration/route/sniff.md @@ -22,6 +22,7 @@ If enabled in the inbound, the protocol and domain name (if present) of by the c | UDP | `dtls` | / | / | | TCP | `ssh` | / | SSH Client Name | | TCP | `rdp` | / | / | +| UDP | `ntp` | / | / | | QUIC Client | Type | |:------------------------:|:----------:| diff --git a/docs/configuration/route/sniff.zh.md b/docs/configuration/route/sniff.zh.md index cd582517..546210c8 100644 --- a/docs/configuration/route/sniff.zh.md +++ b/docs/configuration/route/sniff.zh.md @@ -22,6 +22,7 @@ | UDP | `dtls` | / | / | | TCP | `ssh` | / | SSH 客户端名称 | | TCP | `rdp` | / | / | +| UDP | `ntp` | / | / | | QUIC 客户端 | 类型 | |:------------------------:|:----------:| diff --git a/route/route.go b/route/route.go index fa6858da..531ad039 100644 --- a/route/route.go +++ b/route/route.go @@ -607,6 +607,7 @@ func (r *Router) actionSniff( sniff.UTP, sniff.UDPTracker, sniff.DTLSRecord, + sniff.NTP, } } err = sniff.PeekPacket( diff --git a/route/rule/rule_action.go b/route/rule/rule_action.go index 70363eff..fa8594a0 100644 --- a/route/rule/rule_action.go +++ b/route/rule/rule_action.go @@ -368,6 +368,8 @@ func (r *RuleActionSniff) build() error { r.StreamSniffers = append(r.StreamSniffers, sniff.SSH) case C.ProtocolRDP: r.StreamSniffers = append(r.StreamSniffers, sniff.RDP) + case C.ProtocolNTP: + r.PacketSniffers = append(r.PacketSniffers, sniff.NTP) default: return E.New("unknown sniffer: ", name) }