Skip to content

Commit

Permalink
V2 (#48)
Browse files Browse the repository at this point in the history
* v2 mvp

* minor changes and fixes

* fixed the timer on geoip, added loglevel

* added cidr acl

* Update install.sh

* minor yq fixes

* better acl docs and var names

* allow individual binds for different services

* added ability to generate default config yaml

* clearer parameters in geoip configs

* cleaner installer wizard

* added version info in build

* removed extra whitespace

* ACL overhaul

* tweaked release parameters

* converted defaultconfig to boolean

* removed unused doh client

* doh fixes

* change override log to debug

* doq connects back to the correct upstream

* bugfixes in dns and ACL

* moved back to zerolog and pretty logging

* more fixes in logging format

* fix in debug logging of DoQ

* minor changes in cidr acl logic

* add acl tests (#47)

* add acl tests

* moved yaml configs to the test file

* added back previous tests and fixed the run function

---------

Co-authored-by: Ali Mosajjal <hi@n0p.me>

* acl stringifier

* minor docs changes in the install.sh script

---------

Co-authored-by: Andreas Groll <10852221+holygrolli@users.noreply.github.com>
  • Loading branch information
mosajjal and holygrolli authored May 13, 2023
1 parent b20546a commit 510eee6
Show file tree
Hide file tree
Showing 31 changed files with 2,862 additions and 713 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
- goarch: "386"
goos: darwin
- goarch: "arm"
goos: darwin
goos: darwin
steps:
- uses: actions/checkout@v3
- uses: wangyoucao577/go-release-action@master
Expand All @@ -27,6 +27,6 @@ jobs:
goos: ${{ matrix.goos }}
goarch: ${{ matrix.goarch }}
goversion: 1.20.3
ldflags: "-s -w"
ldflags: "-s -w -X main.version=${{ github.event.release.tag_name }} -X main.commit=${{ github.sha }}"
build_flags: -v

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,6 @@ _testmain.go
*.test
*.prof
sniproxy
config.yaml

.TODO.md
6 changes: 3 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ RUN mkdir /app
ADD . /app/
WORKDIR /app
ENV CGO_ENABLED=0
RUN go build -o main .
CMD ["/app/main"]
RUN go build -ldflags "-s -w -X main.version=$(git describe --tags) -X main.commit=$(git rev-parse HEAD)" -o sniproxy .
CMD ["/app/sniproxy"]

FROM scratch
COPY --from=0 /app/main /sniproxy
COPY --from=0 /app/sniproxy /sniproxy
ENTRYPOINT ["/sniproxy"]
104 changes: 104 additions & 0 deletions acl/acl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package acl

import (
"fmt"
"net"
"sort"

"github.com/knadh/koanf"
"github.com/rs/zerolog"
)

// Decision is the type of decision that an ACL can make for each connection info
type Decision uint8

func (d Decision) String() string {
switch d {
case Accept:
return "Accept"
case Reject:
return "Reject"
case ProxyIP:
return "ProxyIP"
case OriginIP:
return "OriginIP"
case Override:
return "Override"
default:
return "Unknown"
}
}

const (
// Accept shows the indifference of the ACL to the connection
Accept Decision = iota
// Reject shows that the ACL has rejected the connection. each ACL should check this before proceeding to check the connection against its rules
Reject
// ProxyIP shows that the ACL has decided to proxy the connection through sniproxy rather than the origin IP
ProxyIP
// OriginIP shows that the ACL has decided to proxy the connection through the origin IP rather than sniproxy
OriginIP
// Override shows that the ACL has decided to override the connection and proxy it through the specified DstIP and DstPort
Override
)

// ConnInfo contains all the information about a connection that is available
// it also serves as an ACL enforcer in a sense that if IsRejected is set to true
// the connection is dropped
type ConnInfo struct {
SrcIP net.Addr
DstIP net.TCPAddr
Domain string
Decision
}

type ByPriority []ACL

func (a ByPriority) Len() int { return len(a) }
func (a ByPriority) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByPriority) Less(i, j int) bool { return a[i].Priority() < a[j].Priority() }

type ACL interface {
Decide(*ConnInfo) error
Name() string
Priority() uint
ConfigAndStart(*zerolog.Logger, *koanf.Koanf) error
}

// StartACLs starts all the ACLs that have been configured and registered
func StartACLs(log *zerolog.Logger, k *koanf.Koanf) ([]ACL, error) {
var a []ACL
aclK := k.Cut("acl")
for _, acl := range availableACLs {
// cut each konaf based on the name of the ACL
// only configure if the "enabled" key is set to true
if !aclK.Bool(fmt.Sprintf("%s.enabled", (acl).Name())) {
continue
}
var l = log.With().Str("acl", (acl).Name()).Logger()
// we pass the full config to each ACL so that they can cut it themselves. it's needed for some ACLs that need
// to read the config of other ACLs or the global config
if err := acl.ConfigAndStart(&l, k); err != nil {
log.Warn().Msgf("failed to start ACL %s with error %s", (acl).Name(), err)
return a, err
}
a = append(a, acl)
log.Info().Msgf("started ACL: '%s'", (acl).Name())

}
return a, nil
}

// MakeDecision loops through all the ACLs and makes a decision for the connection
func MakeDecision(c *ConnInfo, a []ACL) error {
sort.Sort(ByPriority(a))
for _, acl := range a {
if err := acl.Decide(c); err != nil {
return err
}
}
return nil
}

// each ACL should register itself by appending itself to this list
var availableACLs []ACL
204 changes: 204 additions & 0 deletions acl/acl_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
package acl

import (
"net"
"os"
"testing"
"time"

"github.com/knadh/koanf"
"github.com/knadh/koanf/parsers/yaml"
"github.com/knadh/koanf/providers/rawbytes"
"github.com/rs/zerolog"
)

var logger = zerolog.New(os.Stderr).With().Timestamp().Logger().Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339, NoColor: true})

var configs = map[string]string{
"acl_domain.yaml": `
acl:
domain:
enabled: true
priority: 20
path: ../domains.csv
refresh_interval: 1h0m0s`,
"acl_cidr.yaml": `
acl:
cidr:
enabled: true
priority: 30
path: ../cidr.csv
refresh_interval: 1h0m0s`,
"acl_domain_cidr.yaml": `
acl:
domain:
enabled: true
priority: 20
path: ../domains.csv
refresh_interval: 1h0m0s
cidr:
enabled: true
priority: 30
path: ../cidr.csv
refresh_interval: 1h0m0s`,
"acl_cidr_domain.yaml": `
acl:
domain:
enabled: true
priority: 20
path: ../domains.csv
refresh_interval: 1h0m0s
cidr:
enabled: true
priority: 19
path: ../cidr.csv
refresh_interval: 1h0m0s`,
}

func TestMakeDecision(t *testing.T) {
// Test cases
cases := []struct {
connInfo *ConnInfo
config string
expected Decision
}{
{
// domain in domains.csv
connInfo: mockConnInfo("1.1.1.1", "ipinfo.io"),
config: configs["acl_domain.yaml"],
expected: ProxyIP,
},
{
// domain NOT in domains.csv
connInfo: mockConnInfo("2.2.2.2", "google.de"),
config: configs["acl_domain.yaml"],
expected: OriginIP,
},
{
// ip REJECT in cidr.csv
// if you want to whitelist IPs then you must include "0.0.0.0/0,reject" otherwise always accepted!!
connInfo: mockConnInfo("1.1.1.1", "google.de"),
config: configs["acl_cidr.yaml"],
expected: Reject,
},
{
// ip ACCEPT in cidr.csv
connInfo: mockConnInfo("77.77.1.1", "google.de"),
config: configs["acl_cidr.yaml"],
expected: Accept,
},
{
// ip ACCEPT in cidr.csv, still no ProxyIP (acl.domain not enabled)
connInfo: mockConnInfo("77.77.1.1", "ipinfo.io"),
config: configs["acl_cidr.yaml"],
expected: Accept,
},
{
// domain in domains.csv, ip ACCEPT in cidr.csv
connInfo: mockConnInfo("77.77.1.1", "ipinfo.io"),
config: configs["acl_domain_cidr.yaml"],
expected: ProxyIP,
},
{
// domain NOT in domains.csv, ip ACCEPT in cidr.csv
connInfo: mockConnInfo("77.77.1.1", "google.de"),
config: configs["acl_domain_cidr.yaml"],
expected: OriginIP,
},
{
// domain in domains.csv, ip REJECT in cidr.csv
connInfo: mockConnInfo("1.1.1.1", "ipinfo.io"),
config: configs["acl_domain_cidr.yaml"],
expected: Reject, // still returns OriginIP in DNS !!!
},
{
// domain NOT in domains.csv, ip REJECT in cidr.csv
connInfo: mockConnInfo("1.1.1.1", "google.de"),
config: configs["acl_domain_cidr.yaml"],
expected: Reject, // still returns OriginIP in DNS !!!
},
{
// domain NOT in domains.csv, ip ACCEPT in cidr.csv
connInfo: mockConnInfo("77.77.1.1", "google.de"),
config: configs["acl_cidr_domain.yaml"],
expected: OriginIP,
},
{
// domain in domains.csv, ip ACCEPT in cidr.csv
connInfo: mockConnInfo("77.77.1.1", "ipinfo.io"),
config: configs["acl_cidr_domain.yaml"],
expected: ProxyIP,
},
{
// domain in domains.csv, ip REJECT in cidr.csv
connInfo: mockConnInfo("1.1.1.1", "google.de"),
config: configs["acl_cidr_domain.yaml"],
expected: Reject,
},
{
// domain NOT in domains.csv, ip REJECT in cidr.csv
connInfo: mockConnInfo("1.1.1.1", "google.de"),
config: configs["acl_cidr_domain.yaml"],
expected: Reject,
},
}

// Run the test cases
for _, tc := range cases {
t.Run(tc.config, func(t *testing.T) {
MakeDecision(tc.connInfo, getAcls(&logger, tc.config))
if tc.expected != tc.connInfo.Decision {
t.Errorf("MakeDecision (domain=%v,ip=%v,config=%v) decided %v, expected %v", tc.connInfo.Domain, tc.connInfo.SrcIP, tc.config, tc.connInfo.Decision, tc.expected)
}
})
}
}

// TestReverse tests the reverse function
func TestReverse(t *testing.T) {
tests := []struct {
name string
s string
want string
}{
{name: "test1", s: "abc", want: "cba"},
{name: "test2", s: "a", want: "a"},
{name: "test3", s: "aab", want: "baa"},
{name: "test4", s: "zzZ", want: "Zzz"},
{name: "test5", s: "ab2", want: "2ba"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := reverse(tt.s); got != tt.want {
t.Errorf("reverse() = %v, want %v", got, tt.want)
}
})
}
}

func getAcls(log *zerolog.Logger, config string) []ACL {
var k = koanf.New(".")
if err := k.Load(rawbytes.Provider([]byte(config)), yaml.Parser()); err != nil {
log.Fatal().Msgf("error loading config file: %v", err)
}
a, err := StartACLs(&logger, k)
if err != nil {
panic(err)
}
// we need this to give acl time to (re)load
time.Sleep(1 * time.Second)
return a
}

func mockConnInfo(srcIP string, domain string) *ConnInfo {
addr, err := net.ResolveTCPAddr("tcp", srcIP+":80")

if err != nil {
logger.Fatal().Msgf("error parsing ip from string: %v", err)
}

return &ConnInfo{
SrcIP: addr,
Domain: domain,
}
}
Loading

0 comments on commit 510eee6

Please sign in to comment.