mirror of
https://github.com/evilsocket/shieldwall.git
synced 2021-09-07 00:28:37 +03:00
rule expiration
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -3,4 +3,5 @@ _build
|
||||
.idea
|
||||
dist
|
||||
agent.yaml
|
||||
api.yaml
|
||||
api.yaml
|
||||
database.env
|
||||
15
agent.example.yaml
Normal file
15
agent.example.yaml
Normal 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'
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user