Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ It organizes blocking into **blocks** — named groups of:

When you **activate** a block, a daemon continuously ensures the blocking layers stay applied.

Blocks can also have a **daily time limit**. A limited block stays available while it still has time left for the local day; Open Turkey monitors network traffic to the block's sites, counts traffic activity as usage, and automatically starts blocking the block once the daily quota is exhausted.

## How it works — 4 enforcement layers

Open Turkey doesn't rely on a single, easily-bypassed mechanism. Each active block is enforced on four independent layers, and a `systemd` daemon re-applies them every 5 seconds if anything is tampered with:
Expand Down Expand Up @@ -106,7 +108,37 @@ Unlock a locked block (this also **deactivates** it):
open-turkey unlock social-media
```

### 3) Editing a block's lists
### 3) Daily time limits

Configure a block to allow up to 30 minutes of site traffic per local day:

```bash
open-turkey limit set social-media --daily 30m
open-turkey start social-media --lock
```

With a daily limit configured, you do not need to run a command to unlock time. While the block is active and still has quota left, its sites remain available and Open Turkey installs firewall counting rules. When traffic to those sites is detected, the daemon counts the block as active for 60 seconds. New traffic extends that activity window; once the daily quota is exhausted, the normal blocking layers are applied until the next local day.

Show limit usage:

```bash
open-turkey limit status
open-turkey limit status social-media
```

Remove a limit:

```bash
open-turkey limit remove social-media
```

Notes:

- The first version measures IPv4 network traffic, not browser tabs. Background traffic to a limited domain counts the same as deliberate use.
- Daily limits are meant for site blocks. A limited block must contain at least one site.
- Apps inside a limited block are allowed while the site quota remains and are killed once the quota is exhausted.

### 4) Editing a block's lists

Remove a domain from a block:

Expand Down
2 changes: 2 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ echo "Comandos uteis:"
echo " open-turkey block create <bloco> - Cria um bloco"
echo " open-turkey block add-site <bloco> <dominios...> - Adiciona sites ao bloco"
echo " open-turkey block remove-site <bloco> <dominios...> - Remove sites do bloco"
echo " open-turkey limit set <bloco> --daily 30m - Configura limite diario automatico"
echo " open-turkey limit status [bloco] - Mostra uso do limite diario"
echo " open-turkey start <bloco> - Ativa um bloco"
echo " open-turkey stop <bloco> - Desativa um bloco (se nao estiver travado)"
echo " open-turkey status - Exibe o status dos blocos"
Expand Down
179 changes: 179 additions & 0 deletions internal/blocker/firewall.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import (
"fmt"
"net"
"os/exec"
"strconv"
"strings"
)

Expand All @@ -69,6 +70,16 @@ import (
// ao Open Turkey. O hífen é permitido em nomes de chains.
const chainName = "OPEN-TURKEY"

// limitChainPrefix identifica chains usadas apenas para contar uso de blocos
// com limite diário. Elas não bloqueiam tráfego; só incrementam contadores.
const limitChainPrefix = "OT-LIMIT-"

// LimitTrackTarget descreve um bloco limitado que deve ter tráfego monitorado.
type LimitTrackTarget struct {
BlockID int
Domains []string
}

// dohServersIPv4 contém os endereços IPv4 dos servidores DNS-over-HTTPS
// mais utilizados. Bloqueamos esses IPs na porta 443 (HTTPS) para impedir
// que navegadores façam consultas DNS criptografadas, o que contornaria
Expand Down Expand Up @@ -226,6 +237,86 @@ func RemoveFirewall() error {
return nil
}

// ApplyLimitTracking instala chains de contagem para blocos com limite diário.
func ApplyLimitTracking(targets []LimitTrackTarget) error {
if len(targets) == 0 {
return RemoveLimitTracking()
}

desired := make(map[string]bool)
for _, target := range targets {
if target.BlockID <= 0 {
continue
}

chain := limitChainName(target.BlockID)
desired[chain] = true

_ = runIptables("-N", chain)
if err := runIptables("-F", chain); err != nil {
return fmt.Errorf("erro ao limpar a chain %s: %w", chain, err)
}
if err := ensureOutputJump(chain); err != nil {
return err
}

for _, ip := range resolveDomainsIPv4(target.Domains) {
if err := runIptables("-A", chain, "-d", ip, "-j", "RETURN"); err != nil {
return fmt.Errorf("erro ao adicionar regra de contagem para %s: %w", ip, err)
}
}
}

existing, err := listLimitChains()
if err != nil {
return err
}
for _, chain := range existing {
if !desired[chain] {
removeLimitChain(chain)
}
}

return nil
}

// RemoveLimitTracking remove todas as chains de contagem de uso diário.
func RemoveLimitTracking() error {
chains, err := listLimitChains()
if err != nil {
return err
}
for _, chain := range chains {
removeLimitChain(chain)
}
return nil
}

// ReadLimitTrackingCounters soma os pacotes observados por bloco limitado.
func ReadLimitTrackingCounters() (map[int]uint64, error) {
counters := make(map[int]uint64)

chains, err := listLimitChains()
if err != nil {
return counters, err
}

for _, chain := range chains {
blockID, ok := parseLimitChainID(chain)
if !ok {
continue
}

output, err := runIptablesOutput("-L", chain, "-v", "-x", "-n")
if err != nil {
return counters, err
}
counters[blockID] = parsePacketCounter(output)
}

return counters, nil
}

// IsFirewallApplied verifica se o firewall do Open Turkey está ativo.
//
// Para considerar o firewall como "aplicado", duas condições precisam
Expand Down Expand Up @@ -293,6 +384,94 @@ func BlockDoH() error {
// Funções auxiliares (helpers) — uso interno do pacote
// =============================================================================

func ensureOutputJump(chain string) error {
if err := runIptables("-C", "OUTPUT", "-j", chain); err == nil {
return nil
}
if err := runIptables("-I", "OUTPUT", "-j", chain); err != nil {
return fmt.Errorf("erro ao inserir salto para a chain %s na OUTPUT: %w", chain, err)
}
return nil
}

func removeLimitChain(chain string) {
_ = runIptables("-D", "OUTPUT", "-j", chain)
_ = runIptables("-F", chain)
_ = runIptables("-X", chain)
}

func listLimitChains() ([]string, error) {
output, err := runIptablesOutput("-S")
if err != nil {
return nil, err
}

var chains []string
for _, line := range strings.Split(output, "\n") {
fields := strings.Fields(line)
if len(fields) == 2 && fields[0] == "-N" && strings.HasPrefix(fields[1], limitChainPrefix) {
chains = append(chains, fields[1])
}
}
return chains, nil
}

func limitChainName(blockID int) string {
return fmt.Sprintf("%s%d", limitChainPrefix, blockID)
}

func parseLimitChainID(chain string) (int, bool) {
if !strings.HasPrefix(chain, limitChainPrefix) {
return 0, false
}
id, err := strconv.Atoi(strings.TrimPrefix(chain, limitChainPrefix))
if err != nil || id <= 0 {
return 0, false
}
return id, true
}

func resolveDomainsIPv4(domains []string) []string {
seen := make(map[string]bool)
var ips []string

for _, domain := range domains {
domain = NormalizarDominio(domain)
if domain == "" {
continue
}

resolved, err := net.LookupHost(domain)
if err != nil {
continue
}
for _, ip := range resolved {
if strings.Contains(ip, ":") || seen[ip] {
continue
}
seen[ip] = true
ips = append(ips, ip)
}
}

return ips
}

func parsePacketCounter(output string) uint64 {
var total uint64
for _, line := range strings.Split(output, "\n") {
fields := strings.Fields(line)
if len(fields) == 0 {
continue
}
packets, err := strconv.ParseUint(fields[0], 10, 64)
if err == nil {
total += packets
}
}
return total
}

// runIptables executa um comando iptables com os argumentos fornecidos.
//
// Por que encapsulamos isso em uma função separada?
Expand Down
Loading