mirror of
https://github.com/DanielLavrushin/b4.git
synced 2026-07-10 00:12:25 +00:00
feat(api): add geosite API endpoint and implement geosite tag retrieval
feat(stun): implement STUN message validation and type identification refactor(config): add FilterSTUN option to UDPConfig and update related logic
This commit is contained in:
parent
d00d1d204d
commit
2642a42de3
7 changed files with 174 additions and 68 deletions
|
|
@ -10,7 +10,6 @@ import (
|
|||
"github.com/daniellavrushin/b4/geodat"
|
||||
"github.com/daniellavrushin/b4/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -62,6 +61,7 @@ type UDPConfig struct {
|
|||
DPortMin int `json:"dport_min" bson:"dport_min"`
|
||||
DPortMax int `json:"dport_max" bson:"dport_max"`
|
||||
FilterQUIC string `json:"filter_quic" bson:"filter_quic"`
|
||||
FilterSTUN bool `json:"filter_stun" bson:"filter_stun"`
|
||||
}
|
||||
|
||||
type DomainsConfig struct {
|
||||
|
|
@ -132,6 +132,7 @@ var DefaultConfig = Config{
|
|||
DPortMin: 0,
|
||||
DPortMax: 0,
|
||||
FilterQUIC: "disabled",
|
||||
FilterSTUN: true,
|
||||
},
|
||||
|
||||
WebServer: WebServer{
|
||||
|
|
@ -248,6 +249,7 @@ func (c *Config) BindFlags(cmd *cobra.Command) {
|
|||
cmd.Flags().IntVar(&c.UDP.DPortMin, "udp-dport-min", c.UDP.DPortMin, "Minimum UDP destination port to handle")
|
||||
cmd.Flags().IntVar(&c.UDP.DPortMax, "udp-dport-max", c.UDP.DPortMax, "Maximum UDP destination port to handle")
|
||||
cmd.Flags().StringVar(&c.UDP.FilterQUIC, "udp-filter-quic", c.UDP.FilterQUIC, "QUIC filtering mode (disabled|all|parse)")
|
||||
cmd.Flags().BoolVar(&c.UDP.FilterSTUN, "udp-filter-stun", c.UDP.FilterSTUN, "STUN filtering mode (disabled|all|parse)")
|
||||
|
||||
// Feature flags
|
||||
cmd.Flags().BoolVar(&c.UseGSO, "gso", c.UseGSO, "Enable Generic Segmentation Offload")
|
||||
|
|
@ -280,46 +282,6 @@ func (cfg *Config) ApplyLogLevel(level string) {
|
|||
}
|
||||
}
|
||||
|
||||
func (cfg *Config) ApplyCliOverrides(cmd *cobra.Command) map[string]interface{} {
|
||||
overrides := make(map[string]interface{})
|
||||
|
||||
cmd.Flags().Visit(func(f *pflag.Flag) {
|
||||
// Store the value of each flag that was explicitly set
|
||||
switch f.Value.Type() {
|
||||
case "int":
|
||||
if val, err := cmd.Flags().GetInt(f.Name); err == nil {
|
||||
overrides[f.Name] = val
|
||||
}
|
||||
case "uint":
|
||||
if val, err := cmd.Flags().GetUint(f.Name); err == nil {
|
||||
overrides[f.Name] = val
|
||||
}
|
||||
case "uint8":
|
||||
if val, err := cmd.Flags().GetUint8(f.Name); err == nil {
|
||||
overrides[f.Name] = val
|
||||
}
|
||||
case "int32":
|
||||
if val, err := cmd.Flags().GetInt32(f.Name); err == nil {
|
||||
overrides[f.Name] = val
|
||||
}
|
||||
case "bool":
|
||||
if val, err := cmd.Flags().GetBool(f.Name); err == nil {
|
||||
overrides[f.Name] = val
|
||||
}
|
||||
case "string":
|
||||
if val, err := cmd.Flags().GetString(f.Name); err == nil {
|
||||
overrides[f.Name] = val
|
||||
}
|
||||
case "stringSlice":
|
||||
if val, err := cmd.Flags().GetStringSlice(f.Name); err == nil {
|
||||
overrides[f.Name] = val
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return overrides
|
||||
}
|
||||
|
||||
func (c *Config) Validate() error {
|
||||
|
||||
c.WebServer.IsEnabled = c.WebServer.Port < 0 || c.WebServer.Port > 65535
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@ package geodat
|
|||
import (
|
||||
"bufio"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/daniellavrushin/b4/log"
|
||||
"github.com/urlesistiana/v2dat/v2data"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
|
@ -39,7 +39,7 @@ func UnpackGeoSite(args *UnpackArgs) error {
|
|||
}
|
||||
for tag, domains := range entries {
|
||||
if err := save(tag, domains); err != nil {
|
||||
return fmt.Errorf("failed to save %s: %w", tag, err)
|
||||
return log.Errorf("failed to save %s: %w", tag, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
|
@ -47,16 +47,16 @@ func UnpackGeoSite(args *UnpackArgs) error {
|
|||
|
||||
func readCountryCode(msg []byte) (string, error) {
|
||||
if len(msg) == 0 || msg[0] != 0x0A {
|
||||
return "", fmt.Errorf("bad key")
|
||||
return "", log.Errorf("bad key")
|
||||
}
|
||||
l, n := binary.Uvarint(msg[1:])
|
||||
if n <= 0 {
|
||||
return "", fmt.Errorf("bad varint")
|
||||
return "", log.Errorf("bad varint")
|
||||
}
|
||||
start := 1 + n
|
||||
end := start + int(l)
|
||||
if end > len(msg) {
|
||||
return "", fmt.Errorf("string truncated")
|
||||
return "", log.Errorf("string truncated")
|
||||
}
|
||||
return strings.ToLower(string(msg[start:end])), nil
|
||||
}
|
||||
|
|
@ -83,7 +83,7 @@ func streamGeoSite(file string, filters []string, save func(string, []*v2data.Do
|
|||
return err
|
||||
}
|
||||
if tagByte != 0x0A {
|
||||
return fmt.Errorf("unexpected wire tag %02X", tagByte)
|
||||
return log.Errorf("unexpected wire tag %02X", tagByte)
|
||||
}
|
||||
length, err := binary.ReadUvarint(r)
|
||||
if err != nil {
|
||||
|
|
@ -115,10 +115,12 @@ func streamGeoSite(file string, filters []string, save func(string, []*v2data.Do
|
|||
return nil
|
||||
}
|
||||
|
||||
func ListGeoSiteTags(filePath string) error {
|
||||
func ListGeoSiteTags(filePath string) ([]string, error) {
|
||||
|
||||
log.Tracef("Listing geo site tags from %s", filePath)
|
||||
f, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
|
|
@ -130,22 +132,22 @@ func ListGeoSiteTags(filePath string) error {
|
|||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
if b != 0x0A {
|
||||
return fmt.Errorf("unexpected wire tag %02X", b)
|
||||
return nil, log.Errorf("unexpected wire tag %02X", b)
|
||||
}
|
||||
l, err := binary.ReadUvarint(r)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, log.Errorf("failed to read varint: %w", err)
|
||||
}
|
||||
msg := make([]byte, l)
|
||||
if _, err := io.ReadFull(r, msg); err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
tag, err := readCountryCode(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
set[tag] = struct{}{}
|
||||
}
|
||||
|
|
@ -155,10 +157,8 @@ func ListGeoSiteTags(filePath string) error {
|
|||
tags = append(tags, t)
|
||||
}
|
||||
sort.Strings(tags)
|
||||
for _, t := range tags {
|
||||
fmt.Println(t)
|
||||
}
|
||||
return nil
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
func convertV2DomainToText(dom []*v2data.Domain, w io.Writer) error {
|
||||
|
|
@ -181,13 +181,3 @@ func convertV2DomainToText(dom []*v2data.Domain, w io.Writer) error {
|
|||
_, err := io.WriteString(w, b.String())
|
||||
return err
|
||||
}
|
||||
|
||||
func convertV2DomainToTextFile(domain []*v2data.Domain, file string) error {
|
||||
f, err := os.Create(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
return convertV2DomainToText(domain, f)
|
||||
}
|
||||
|
|
|
|||
47
src/http/handler/geosite.go
Normal file
47
src/http/handler/geosite.go
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/daniellavrushin/b4/config"
|
||||
"github.com/daniellavrushin/b4/geodat"
|
||||
"github.com/daniellavrushin/b4/log"
|
||||
)
|
||||
|
||||
func RegisterGeositeApi(mux *http.ServeMux, cfg *config.Config) {
|
||||
api := &API{cfg: cfg}
|
||||
mux.HandleFunc("/api/geosite", api.handleGeoSite)
|
||||
}
|
||||
|
||||
func (a *API) handleGeoSite(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
a.getGeositeTags(w)
|
||||
default:
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *API) getGeositeTags(w http.ResponseWriter) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
enc := json.NewEncoder(w)
|
||||
|
||||
if a.cfg.Domains.GeoSitePath == "" {
|
||||
log.Tracef("Geosite path is not configured")
|
||||
_ = enc.Encode(GeositeResponse{Tags: []string{}})
|
||||
return
|
||||
}
|
||||
|
||||
tags, err := geodat.ListGeoSiteTags(a.cfg.Domains.GeoSitePath)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to load geosite tags: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := GeositeResponse{
|
||||
Tags: tags,
|
||||
}
|
||||
|
||||
_ = enc.Encode(response)
|
||||
}
|
||||
|
|
@ -7,3 +7,7 @@ import (
|
|||
type API struct {
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
type GeositeResponse struct {
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ func registerWebSocketEndpoints(mux *stdhttp.ServeMux) {
|
|||
func registerAPIEndpoints(mux *stdhttp.ServeMux, cfg *config.Config) {
|
||||
handler.RegisterConfigApi(mux, cfg)
|
||||
handler.RegisterMetricsApi(mux, cfg)
|
||||
handler.RegisterGeositeApi(mux, cfg)
|
||||
|
||||
log.Infof("REST API endpoints registered")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import (
|
|||
"github.com/daniellavrushin/b4/metrics"
|
||||
"github.com/daniellavrushin/b4/sni"
|
||||
"github.com/daniellavrushin/b4/sock"
|
||||
"github.com/daniellavrushin/b4/stun"
|
||||
"github.com/florianl/go-nfqueue"
|
||||
)
|
||||
|
||||
|
|
@ -216,6 +217,13 @@ func (w *Worker) Start() error {
|
|||
sport := binary.BigEndian.Uint16(udp[0:2])
|
||||
dport := binary.BigEndian.Uint16(udp[2:4])
|
||||
|
||||
if cfg.UDP.FilterSTUN && stun.IsSTUNMessage(payload) {
|
||||
// Log at TRACE level to avoid spam (STUN is very frequent)
|
||||
log.Tracef("STUN %s:%d -> %s:%d", src.String(), sport, dst.String(), dport)
|
||||
_ = q.SetVerdict(id, nfqueue.NfAccept)
|
||||
return 0
|
||||
}
|
||||
|
||||
host := ""
|
||||
if h, ok := sni.ParseQUICClientHelloSNI(payload); ok {
|
||||
host = h
|
||||
|
|
|
|||
94
src/stun/stun.go
Normal file
94
src/stun/stun.go
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
// path: src/stun/stun.go
|
||||
package stun
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
)
|
||||
|
||||
// IsSTUNMessage checks if a UDP payload is a STUN message (RFC 5389)
|
||||
func IsSTUNMessage(data []byte) bool {
|
||||
// STUN header is 20 bytes:
|
||||
// 2 (type) + 2 (length) + 4 (magic cookie) + 12 (transaction ID)
|
||||
if len(data) < 20 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Read message type (big endian, network byte order)
|
||||
messageType := binary.BigEndian.Uint16(data[0:2])
|
||||
|
||||
// Read message length (big endian)
|
||||
// This is the length of the message body, NOT including the 20-byte header
|
||||
messageLength := binary.BigEndian.Uint16(data[2:4])
|
||||
|
||||
// Validate that the packet length matches the declared message length
|
||||
// Total packet size should be: 20 (header) + messageLength (body)
|
||||
if len(data) != int(messageLength)+20 {
|
||||
return false
|
||||
}
|
||||
|
||||
// RFC 5389 Section 6: The most significant 2 bits of every STUN message MUST be zeros
|
||||
// This helps distinguish STUN from other protocols using the same ports
|
||||
if (messageType & 0xC000) != 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Filter STUN requests only (not responses or indications)
|
||||
// Message Type bits:
|
||||
// Bit 4 (0x0010): 0=request, 1=indication
|
||||
// Bit 8 (0x0100): 0=request/indication, 1=response/error response
|
||||
// We want to detect only requests (both bits = 0)
|
||||
// This prevents mangling STUN responses which could break WebRTC
|
||||
if (messageType & 0x0110) != 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check magic cookie (RFC 5389 Section 6)
|
||||
// Magic cookie is 0x2112A442 (in network byte order)
|
||||
// This is a fixed value that helps identify STUN messages
|
||||
magicCookie := binary.BigEndian.Uint32(data[4:8])
|
||||
if magicCookie != 0x2112A442 {
|
||||
return false
|
||||
}
|
||||
|
||||
// All checks passed - this is a STUN request
|
||||
return true
|
||||
}
|
||||
|
||||
// GetSTUNMessageType returns the STUN message type if it's a valid STUN message
|
||||
// Returns 0 if not a STUN message
|
||||
func GetSTUNMessageType(data []byte) uint16 {
|
||||
if !IsSTUNMessage(data) {
|
||||
return 0
|
||||
}
|
||||
return binary.BigEndian.Uint16(data[0:2]) & 0x3FFF
|
||||
}
|
||||
|
||||
// Common STUN message types (for debugging/logging)
|
||||
const (
|
||||
BindingRequest = 0x0001
|
||||
BindingResponse = 0x0101
|
||||
BindingErrorResponse = 0x0111
|
||||
SharedSecretRequest = 0x0002
|
||||
SharedSecretResponse = 0x0102
|
||||
SharedSecretErrorResponse = 0x0112
|
||||
)
|
||||
|
||||
// MessageTypeName returns a human-readable name for STUN message types
|
||||
func MessageTypeName(msgType uint16) string {
|
||||
switch msgType {
|
||||
case BindingRequest:
|
||||
return "Binding Request"
|
||||
case BindingResponse:
|
||||
return "Binding Response"
|
||||
case BindingErrorResponse:
|
||||
return "Binding Error Response"
|
||||
case SharedSecretRequest:
|
||||
return "Shared Secret Request"
|
||||
case SharedSecretResponse:
|
||||
return "Shared Secret Response"
|
||||
case SharedSecretErrorResponse:
|
||||
return "Shared Secret Error Response"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue