1
0
mirror of https://github.com/evilsocket/shieldwall.git synced 2021-09-07 00:28:37 +03:00

rule expiration

This commit is contained in:
Simone Margaritelli
2021-02-10 01:02:30 +01:00
parent d575143552
commit 6b562acc88
14 changed files with 209 additions and 61 deletions

3
.gitignore vendored
View File

@@ -3,4 +3,5 @@ _build
.idea
dist
agent.yaml
api.yaml
api.yaml
database.env

15
agent.example.yaml Normal file
View File

@@ -0,0 +1,15 @@
# where to store the lists
data: '/var/lib/shieldwall/'
# path to iptables
iptables: '/sbin/iptables'
# api server to use
server: 'https://api.shieldwall.me'
# authentication token
token: 'deadbeefdeadbeef'
# api polling period in seconds
period: 10
# api timeout in seconds or 0 for no timeout
timeout: 0
# list of ip addresses to always allow just in case
allow:
- '127.0.0.1'

View File

@@ -1,8 +1,10 @@
package api
import (
"encoding/json"
"github.com/evilsocket/islazy/log"
"github.com/evilsocket/shieldwall/database"
"github.com/evilsocket/shieldwall/firewall"
"gorm.io/datatypes"
"net/http"
"sync"
@@ -16,6 +18,30 @@ type cachedRules struct {
var cacheByAgentToken = sync.Map{}
func (api *API) expireRules(jsonbRules datatypes.JSON, doLog bool) (datatypes.JSON, int, error) {
var rules []firewall.Rule
expired := 0
notExpired := make([]firewall.Rule, 0)
if err := json.Unmarshal(jsonbRules, &rules); err != nil {
return nil, 0, err
}
for _, rule := range rules {
if rule.Expired() {
if doLog {
log.Info("rule expired %#v", rule)
}
expired++
} else {
notExpired = append(notExpired, rule)
}
}
return database.ToJSONB(notExpired), expired, nil
}
func (api *API) GetRules(w http.ResponseWriter, r *http.Request) {
agentIP := clientIP(r)
agentToken := r.Header.Get("X-ShieldWall-Agent-Token")
@@ -27,6 +53,8 @@ func (api *API) GetRules(w http.ResponseWriter, r *http.Request) {
return
}
cacheWhat := "miss"
// check cache first
entry, found := cacheByAgentToken.Load(agentToken)
if found {
@@ -36,13 +64,27 @@ func (api *API) GetRules(w http.ResponseWriter, r *http.Request) {
log.Debug("agent cache expired")
cacheByAgentToken.Delete(agentToken)
} else {
w.Header().Set("shieldwall-cache", "hit")
JSON(w, http.StatusOK, cached.Rules)
return
// check expired rules
_, expired, err := api.expireRules(cached.Rules, false)
if err != nil {
log.Error("error checking rules expiration: %v", err)
JSON(w, http.StatusInternalServerError, nil)
return
}
// bypass and invalidate cache if there are expired rules
// in order to cache a fresh copy of the model
if expired == 0 {
w.Header().Set("shieldwall-cache", "hit")
JSON(w, http.StatusOK, cached.Rules)
return
} else {
cacheByAgentToken.Delete(agentToken)
cacheWhat = "purge" // let the client know what happened ^_^
}
}
}
w.Header().Set("shieldwall-cache", "miss")
w.Header().Set("shieldwall-cache", cacheWhat)
agent, err := database.FindAgentByToken(agentToken)
if err != nil {
@@ -55,11 +97,13 @@ func (api *API) GetRules(w http.ResponseWriter, r *http.Request) {
return
}
// save to cache
cacheByAgentToken.Store(agentToken, &cachedRules{
CachedAt: time.Now(),
Rules: agent.Rules,
})
// check expired rules
agent.Rules, _, err = api.expireRules(agent.Rules, true)
if err != nil {
log.Error("error checking rules expiration: %v", err)
JSON(w, http.StatusInternalServerError, nil)
return
}
// log.Debug("[%s %s] successfully authenticated", agentIP, agentUA)
@@ -71,5 +115,11 @@ func (api *API) GetRules(w http.ResponseWriter, r *http.Request) {
log.Error("error updating agent: %v", err)
}
// save to cache
cacheByAgentToken.Store(agentToken, &cachedRules{
CachedAt: time.Now(),
Rules: agent.Rules,
})
JSON(w, http.StatusOK, agent.Rules)
}

View File

@@ -8,11 +8,12 @@ type EmailConfig struct {
}
type Config struct {
URL string `yaml:"url"`
Address string `yaml:"address"`
ReqMaxSize int64 `yaml:"req_max_size"`
TokenTTL int64 `yaml:"token_ttl"`
Secret string `yaml:"secret"`
MaxAgents int64 `yaml:"max_agents_per_user"` // TODO: implement check
CacheTTL int64 `yaml:"cache_ttl"`
URL string `yaml:"url"`
Address string `yaml:"address"`
ReqMaxSize int64 `yaml:"req_max_size"`
TokenTTL int64 `yaml:"token_ttl"`
Secret string `yaml:"secret"`
MaxAgents int64 `yaml:"max_agents_per_user"` // TODO: implement check
CacheTTL int64 `yaml:"cache_ttl"`
AllowNewUsers bool `yaml:"allow_new_users"`
}

View File

@@ -64,16 +64,6 @@ func (api *API) UserCreateAgent(w http.ResponseWriter, r *http.Request) {
return
}
// add the client ip by default if no rule has been provided
if len(req.Rules) == 0 {
req.Rules = []firewall.Rule{{
Type: firewall.RuleAllow,
Address: client,
Protocol: firewall.ProtoAll,
Ports: []string{firewall.AllPorts},
}}
}
agent, err := database.RegisterAgent(user, req.Name, req.Rules)
if err != nil {
ERROR(w, http.StatusBadRequest, err)

View File

@@ -15,6 +15,11 @@ type UserRegisterRequest struct {
}
func (api *API) UserRegister(w http.ResponseWriter, r *http.Request) {
if api.config.AllowNewUsers == false {
ERROR(w, http.StatusLocked, fmt.Errorf("apologies, registrations are closed at the moment"))
return
}
var req UserRegisterRequest
defer r.Body.Close()

View File

@@ -58,6 +58,10 @@ func validateAgentData(name string, rules []firewall.Rule) error {
return fmt.Errorf("%s is not a valid address", rule.Address)
}
if rule.TTL < 0 {
return fmt.Errorf("really? %d", rule.TTL)
}
for _, port := range rule.Ports {
if strings.Index(port, ":") != -1 {
// parse as range
@@ -99,11 +103,15 @@ func RegisterAgent(user *User, name string, rules []firewall.Rule) (*Agent, erro
return nil, fmt.Errorf("agent name already used")
}
for i := range rules {
rules[i].CreatedAt = time.Now()
}
newAgent := Agent{
UserID: user.ID,
Name: name,
Token: makeRandomToken(),
Rules: toJSONB(rules),
Rules: ToJSONB(rules),
}
if err = db.Create(&newAgent).Error; err != nil {
@@ -124,8 +132,14 @@ func UpdateAgent(agent *Agent, name string, rules []firewall.Rule) error {
return fmt.Errorf("agent name already used")
}
for i, rule := range rules {
if rule.TTL > 0 && rule.CreatedAt.IsZero() {
rules[i].CreatedAt = time.Now()
}
}
agent.Name = name
agent.Rules = toJSONB(rules)
agent.Rules = ToJSONB(rules)
agent.UpdatedAt = time.Now()
return db.Save(agent).Error

View File

@@ -20,7 +20,7 @@ func (j *JSONB) Scan(value interface{}) error {
return nil
}
func toJSONB(v interface{}) datatypes.JSON {
func ToJSONB(v interface{}) datatypes.JSON {
data, _ := json.Marshal(v)
return datatypes.JSON(data)
}

View File

@@ -1,5 +1,7 @@
package firewall
import "time"
const (
AllPorts = "1:65535"
)
@@ -15,13 +17,23 @@ const (
type RuleType string
const (
RuleBlock = RuleType("block") // not used for now
RuleBlock = RuleType("block") // TODO: implement block support
RuleAllow = RuleType("allow")
)
type Rule struct {
Type RuleType `json:"type"` // always RuleBlock for now
Address string `json:"address"`
Protocol Protocol `json:"protocol"`
Ports []string `json:"ports"` // strings to also allow ranges
}
CreatedAt time.Time `json:"created_at"`
TTL int `json:"ttl"` // used from the api to delete expired rules
Type RuleType `json:"type"` // always RuleBlock for now
Address string `json:"address"`
Protocol Protocol `json:"protocol"`
Ports []string `json:"ports"` // strings to also allow ranges
}
func (r Rule) Expires() bool {
return r.TTL > 0
}
func (r Rule) Expired() bool {
return r.Expires() && time.Since(r.CreatedAt).Seconds() >= float64(r.TTL)
}

View File

@@ -1,8 +1,9 @@
export default class Rule {
constructor(type, address, protocol, ports) {
constructor(type, address, protocol, ports, ttl) {
this.type = type;
this.address = address;
this.protocol = protocol;
this.ports = ports;
this.ttl = ttl;
}
}

View File

@@ -1,7 +1,7 @@
<template>
<div class="agent-container container-fluid">
<h2>{{ editing ? 'Edit' : 'Create' }} Agent</h2>
<h2>{{ editing ? 'Edit' : 'New' }} Agent</h2>
<br/>
@@ -62,6 +62,7 @@
<th scope="col">Address</th>
<th scope="col">Proto</th>
<th scope="col">Ports</th>
<th scope="col">Expires</th>
<th scope="col"></th>
</tr>
</thead>
@@ -100,6 +101,20 @@
/>
</td>
<td class="fit">
<select class="form-control" v-model.number="rule.ttl" type="number">
<option :selected="rule.ttl == 0" value=0>Never</option>
<option :selected="rule.ttl == 3" value=3>3 Seconds</option>
<option :selected="rule.ttl == 300" value=300>5 Minutes</option>
<option :selected="rule.ttl == 600" value=600>10 Minutes</option>
<option :selected="rule.ttl == 900" value=900>15 Minutes</option>
<option :selected="rule.ttl == 1800" value=1800>30 Minutes</option>
<option :selected="rule.ttl == 3600" value=3600>1 Hour</option>
<option :selected="rule.ttl == 43200" value=43200>12 Hours</option>
<option :selected="rule.ttl == 86400" value=86400>24 Hours</option>
</select>
</td>
<td class="fit">
<a class="btn btn-sm btn-danger" href="#" v-on:click="handleRuleDelete(agent.rules.indexOf(rule))">
x
@@ -142,7 +157,8 @@ export default {
"allow",
this.$store.state.auth.user.address,
"all",
["1:65535"]
["1:65535"],
0
)]),
loading: false,
submitted: false,
@@ -164,7 +180,7 @@ export default {
return;
}
if(this.$route.params.id) {
if (this.$route.params.id) {
this.editing = true;
UserService.getAgent(this.$route.params.id).then(
response => {
@@ -194,8 +210,13 @@ export default {
let method = this.editing ? UserService.updateAgent : UserService.createAgent;
method(this.agent).then(
() => {
this.$router.push('/agents');
response => {
if (this.editing) {
this.$router.push('/agents');
} else {
// force reload
window.location.href = '/agent/' + response.data.id;
}
},
error => {
this.error =
@@ -215,7 +236,8 @@ export default {
"allow",
this.$store.state.auth.user.address,
"all",
["443", "80"]
["443", "80", "22"],
43200
));
}
}

View File

@@ -3,14 +3,16 @@
<a class="btn btn-sm btn-success"
style="margin-bottom: 10px"
:href="'/agents/new'">
:href="'/agents/new'"
v-if="agents.length"
>
new agent
</a>
<br/>
<div class="jumbotron" v-if="!agents.length">
No agents yet.
No agents yet, <a href="/agents/new">create the first one</a>!
</div>
<div v-if="message" class="alert alert-success" role="alert">{{ message }}</div>

View File

@@ -1,24 +1,59 @@
<template>
<div class="home-container container-fluid">
<div class="jumbotron">
<h1 class="display-4">
<img src="/logo.png" width="50px"/>
shieldwall!
</h1>
<br/>
<p class="lead">
Shieldwall is a free service that helps you secure your most private servers.
</p>
<hr class="my-4">
<p>Every agent connects to our API
and fetch a list of the IP addresses you allowed to navigate the host and block the inbound traffic from
every other source.</p>
<p class="lead" v-if="!currentUser">
<div style="text-align: center;">
<h1 class="display-4">
<img src="/logo.png" width="50px"/>
shieldwall
</h1>
<br/>
<a class="btn btn-primary btn" href="/register" role="button">
Create a Free Account
</a>
<p class="lead">
Helps you secure your most private servers!
</p>
</div>
<hr class="my-4">
<p style="text-align: center;">
The shieldwall agent will instrument your server firewall and block inbound connections from every IP, only
allowing the addresses and ports you configured via the backend.
</p>
<div class="container">
<div class="row">
<div class="col">
<div class="card text-center">
<div class="card-header">
Free API
</div>
<div class="card-body">
<p class="card-text">Create here an account for free and use this server to instrument your agents.</p>
<a href="/register" class="btn btn-primary" v-if="!currentUser">Sign Up</a>
&nbsp;
<a href="/docs" class="btn btn-info" >Quick Start</a>
</div>
</div>
</div>
<div class="col">
<div class="card text-center">
<div class="card-header">
Host it Yourself
</div>
<div class="card-body">
<p class="card-text">Host the backend and API service yourself and configure
the agents to communicate in a more discreet setup.</p>
<a href="https://github.com/evilsocket/shieldwall" target="_blank" class="btn btn-success">
Code Repository
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>