Like Prometheus, but for logs.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
loki/pkg/logql/log/ip.go

299 lines
7.5 KiB

package log
import (
"errors"
"fmt"
"net/netip"
"unicode"
"github.com/prometheus/prometheus/model/labels"
"go4.org/netipx"
)
var (
ErrIPFilterInvalidPattern = errors.New("ip: invalid pattern")
ErrIPFilterInvalidOperation = errors.New("ip: invalid operation")
)
type IPMatchType int
const (
IPv4Charset = "0123456789."
IPv6Charset = "0123456789abcdefABCDEF:."
)
// Should be one of the netip.Addr, netip.Prefix, netipx.IPRange.
type IPMatcher interface{}
type IPLineFilter struct {
ip *ipFilter
ty labels.MatchType
}
// NewIPLineFilter is used to construct ip filter as a `LineFilter`
func NewIPLineFilter(pattern string, ty labels.MatchType) (*IPLineFilter, error) {
// check if `ty` supported in ip matcher.
switch ty {
case labels.MatchEqual, labels.MatchNotEqual:
default:
return nil, ErrIPFilterInvalidOperation
}
ip, err := newIPFilter(pattern)
if err != nil {
return nil, err
}
return &IPLineFilter{
ip: ip,
ty: ty,
}, nil
}
// Filter implement `Filterer` interface. Used by `LineFilter`
func (f *IPLineFilter) Filter(line []byte) bool {
return f.filterTy(line, f.ty)
}
// ToStage implements `Filterer` interface.
func (f *IPLineFilter) ToStage() Stage {
return f
}
// `Process` implements `Stage` interface
func (f *IPLineFilter) Process(_ int64, line []byte, _ *LabelsBuilder) ([]byte, bool) {
return line, f.filterTy(line, f.ty)
}
// `RequiredLabelNames` implements `Stage` interface
func (f *IPLineFilter) RequiredLabelNames() []string {
return []string{} // empty for line filter
}
func (f *IPLineFilter) filterTy(line []byte, ty labels.MatchType) bool {
if ty == labels.MatchNotEqual {
return !f.ip.filter(line)
}
return f.ip.filter(line)
}
type IPLabelFilter struct {
ip *ipFilter
Ty LabelFilterType
// if used as Label matcher, this holds the identifier Label name.
// e.g: (|remote_addr = ip("xxx")). Here labelName is `remote_addr`
Label string
// patError records if given pattern is invalid.
patError error
// local copy of Pattern to display it in errors, even though Pattern matcher fails because of invalid Pattern.
Pattern string
}
// NewIPLabelFilter is used to construct ip filter as label filter for the given `label`.
func NewIPLabelFilter(pattern, label string, ty LabelFilterType) *IPLabelFilter {
ip, err := newIPFilter(pattern)
return &IPLabelFilter{
ip: ip,
Label: label,
Ty: ty,
patError: err,
Pattern: pattern,
}
}
// `Process` implements `Stage` interface
func (f *IPLabelFilter) Process(_ int64, line []byte, lbs *LabelsBuilder) ([]byte, bool) {
return line, f.filterTy(line, f.Ty, lbs)
}
func (f *IPLabelFilter) isLabelFilterer() {}
// `RequiredLabelNames` implements `Stage` interface
func (f *IPLabelFilter) RequiredLabelNames() []string {
return []string{f.Label}
}
// PatternError will be used `labelFilter.Stage()` method so that, if the given pattern is wrong
// it returns proper 400 error to the client of LogQL.
func (f *IPLabelFilter) PatternError() error {
return f.patError
}
func (f *IPLabelFilter) filterTy(_ []byte, ty LabelFilterType, lbs *LabelsBuilder) bool {
if lbs.HasErr() {
// why `true`?. if there's an error only the string matchers can filter out.
return true
}
input, ok := lbs.Get(f.Label)
if !ok {
// we have not found the label.
return false
}
if f.ip == nil {
return false
}
switch ty {
case LabelFilterEqual:
return f.ip.filter([]byte(input))
case LabelFilterNotEqual:
return !f.ip.filter([]byte(input))
}
return false
}
// `String` implements fmt.Stringer inteface, by which also implements `LabelFilterer` inteface.
func (f *IPLabelFilter) String() string {
eq := "=" // LabelFilterEqual -> "==", we don't want in string representation of ip label filter.
if f.Ty == LabelFilterNotEqual {
eq = LabelFilterNotEqual.String()
}
return fmt.Sprintf("%s%sip(%q)", f.Label, eq, f.Pattern) // label filter
}
// ipFilter search for IP addresses of given `pattern` in the given `line`.
// It returns true if pattern is matched with at least one IP in the `line`
// pattern - can be of the following form for both IPv4 and IPv6.
// 1. SINGLE-IP - "192.168.0.1"
// 2. IP RANGE - "192.168.0.1-192.168.0.23"
// 3. CIDR - "192.168.0.0/16"
type ipFilter struct {
pattern string
matcher IPMatcher
}
func newIPFilter(pattern string) (*ipFilter, error) {
filter := &ipFilter{pattern: pattern}
matcher, err := getMatcher(pattern)
if err != nil {
return nil, err
}
filter.matcher = matcher
return filter, nil
}
// filter does the heavy lifting finding ip `pattern` in the givin `line`.
// This is the function if you want to understand how the core logic how ip filter works!
func (f *ipFilter) filter(line []byte) bool {
if len(line) == 0 {
return false
}
n := len(line)
filterFn := func(line []byte, start int, charset string) (bool, int) {
iplen := bytesSpan(line[start:], []byte(charset))
if iplen < 0 {
return false, 0
}
ip, err := netip.ParseAddr(string(line[start : start+iplen]))
if err == nil {
if containsIP(f.matcher, ip) {
return true, 0
}
}
return false, iplen
}
// This loop try to extract IPv4 or IPv6 address from the arbitrary string.
// It uses IPv4 and IPv6 prefix hints to find the IP addresses faster without using regexp.
for i := 0; i < n; i++ {
if i+3 < n && ipv4Hint([4]byte{line[i], line[i+1], line[i+2], line[i+3]}) {
ok, iplen := filterFn(line, i, IPv4Charset)
if ok {
return true
}
i += iplen
continue
}
if i+4 < n && ipv6Hint([5]byte{line[i], line[i+1], line[i+2], line[i+3], line[i+4]}) {
ok, iplen := filterFn(line, i, IPv6Charset)
if ok {
return true
}
i += iplen
continue
}
}
return false
}
func containsIP(matcher IPMatcher, ip netip.Addr) bool {
switch m := matcher.(type) {
case netip.Addr:
return m.Compare(ip) == 0
case netipx.IPRange:
return m.Contains(ip)
case netip.Prefix:
return m.Contains(ip)
}
return false
}
func getMatcher(pattern string) (IPMatcher, error) {
var (
matcher IPMatcher
err error
)
matcher, err = netip.ParseAddr(pattern) // is it simple single IP?
if err == nil {
return matcher, nil
}
matcher, err = netip.ParsePrefix(pattern) // is it cidr format? (192.168.0.1/16)
if err == nil {
return matcher, nil
}
matcher, err = netipx.ParseIPRange(pattern) // is it IP range format? (192.168.0.1 - 192.168.4.5
if err == nil {
return matcher, nil
}
return nil, fmt.Errorf("%w: %q", ErrIPFilterInvalidPattern, pattern)
}
func ipv4Hint(prefix [4]byte) bool {
return unicode.IsDigit(rune(prefix[0])) && (prefix[1] == '.' || prefix[2] == '.' || prefix[3] == '.')
}
func ipv6Hint(prefix [5]byte) bool {
hint1 := prefix[0] == ':' && prefix[1] == ':' && isHexDigit(prefix[2])
hint2 := isHexDigit(prefix[0]) && prefix[1] == ':'
hint3 := isHexDigit(prefix[0]) && isHexDigit(prefix[1]) && prefix[2] == ':'
hint4 := isHexDigit(prefix[0]) && isHexDigit(prefix[1]) && isHexDigit(prefix[2]) && prefix[3] == ':'
hint5 := isHexDigit(prefix[0]) && isHexDigit(prefix[1]) && isHexDigit(prefix[2]) && isHexDigit(prefix[3]) && prefix[4] == ':'
return hint1 || hint2 || hint3 || hint4 || hint5
}
func isHexDigit(r byte) bool {
return unicode.IsDigit(rune(r)) || (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F')
}
// bytesSpan is same as C's `strcspan()` function.
// It returns the number of chars in the initial segment of `s`
// which consist only of chars from `accept`.
func bytesSpan(s, accept []byte) int {
m := make(map[byte]bool)
for _, r := range accept {
m[r] = true
}
for i, r := range s {
if !m[r] {
return i
}
}
return len(s)
}