Skip to content

Commit

Permalink
implement outbound rules, fix #27, #30
Browse files Browse the repository at this point in the history
  • Loading branch information
shinebayar-g committed Nov 10, 2021
1 parent 633a165 commit e6ad513
Show file tree
Hide file tree
Showing 2 changed files with 156 additions and 42 deletions.
75 changes: 49 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ This project solves that problem by listening to the Docker API events.

## Supported labels

| Label key | Value / Syntax | Example |
| -------------- | --------------------------------------------------------------- | ----------------------------------------------------------- |
| UFW_MANAGED\* | TRUE | `-l UFW_MANAGED=TRUE` |
| UFW_ALLOW_FROM | CIDR/IP-SpecificPort-Comment , Semicolon separated, default=any | `-l UFW_ALLOW_FROM=192.168.3.0/24-LAN;10.10.0.50/32-53-DNS` |
| Label key | Value / Syntax | Example |
| -------------- | ---------------------------------------------------------------- | ----------------------------------------------------------- |
| UFW_MANAGED | TRUE _(Required for all rules)_ | `-l UFW_MANAGED=TRUE` |
| UFW_ALLOW_FROM | CIDR/IP-SpecificPort-Comment , Semicolon separated, default=any | `-l UFW_ALLOW_FROM=192.168.3.0/24-LAN;10.10.0.50/32-53-DNS` |
| UFW_DENY_OUT | TRUE _(Required if outbound rules are defined)_ | `-l UFW_DENY_OUT=TRUE` |
| UFW_ALLOW_TO | CIDR/IP-SpecificPort-Comment , Semicolon separated, default=none | `-l UFW_ALLOW_TO=192.168.3.0/24-LAN;10.10.0.50/32-53-DNS` |

## Example

Expand All @@ -39,6 +41,8 @@ services:
labels:
UFW_MANAGED: 'TRUE'
UFW_ALLOW_FROM: '172.10.50.32;192.168.3.0/24;10.10.0.50/32-8080-LAN'
UFW_DENY_OUT: 'TRUE'
UFW_ALLOW_TO: '8.8.8.8-53-GoogleDNS;1.1.1.0/24-53-CloudflareDNS;192.168.10.24-8080-LAN'
networks:
- my-network

Expand All @@ -51,17 +55,23 @@ networks:
# Allow from any
➜ docker run -d -p 8080:80 -p 8081:81 -l UFW_MANAGED=TRUE nginx:alpine

# Allow from any + deny all out
➜ docker run -d -p 8082:82 -p 8083:83 -l UFW_MANAGED=TRUE -l UFW_DENY_OUT=TRUE nginx:alpine

# Allow from certain IP address
➜ docker run -d -p 8082:82 -p 8083:83 -l UFW_MANAGED=TRUE -l UFW_ALLOW_FROM=192.168.3.0 nginx:alpine
➜ docker run -d -p 8084:84 -p 8085:85 -l UFW_MANAGED=TRUE -l UFW_ALLOW_FROM=192.168.3.0 nginx:alpine

# Allow from certain CIDR ranges
➜ docker run -d -p 8084:84 -p 8085:85 -l UFW_MANAGED=TRUE -l UFW_ALLOW_FROM="192.168.3.0/24;10.10.0.50/32" nginx:alpine
➜ docker run -d -p 8086:86 -p 8087:87 -l UFW_MANAGED=TRUE -l UFW_ALLOW_FROM="192.168.3.0/24;10.10.0.50/32" nginx:alpine

# Allow from certain IP address, CIDR ranges + comments
➜ docker run -d -p 8086:86 -p 8087:87 -l UFW_MANAGED=TRUE -l UFW_ALLOW_FROM="172.10.5.0;192.168.3.0/24-LAN;10.10.0.50/32-DNS" nginx:alpine
➜ docker run -d -p 8088:88 -p 8089:89 -l UFW_MANAGED=TRUE -l UFW_ALLOW_FROM="172.10.5.0;192.168.3.0/24-LAN;10.10.0.50/32-DNS" nginx:alpine

# Allow from certain IP address, CIDR ranges + deny all out + allow to some IP range (specific port defined) + comments
➜ docker run -d -p 8090:90 -p 8091:91 -l UFW_MANAGED=TRUE -l UFW_ALLOW_FROM="172.10.5.0;192.168.3.0/24-LAN;10.10.0.50/32-DNS" -l UFW_DENY_OUT=TRUE -l UFW_ALLOW_TO="8.8.8.8-53-GoogleDNS;1.1.1.0/24-53-CloudflareDNS;192.168.10.0/24-LAN" nginx:alpine

# Allow from certain IP address, CIDR ranges to different Port + comments
➜ docker run -d -p 8088:88 -p 8089:89 -p 8090:90 -l UFW_MANAGED=TRUE -l UFW_ALLOW_FROM="0.0.0.0/0-88-Internet;192.168.3.0/24-89-LAN;10.10.0.50-90" nginx:alpine
➜ docker run -d -p 8092:92 -p 8093:93 -p 8094:94 -l UFW_MANAGED=TRUE -l UFW_ALLOW_FROM="0.0.0.0/0-88-Internet;192.168.3.0/24-89-LAN;10.10.0.50-90" nginx:alpine

# Results
➜ sudo ufw status
Expand All @@ -71,23 +81,36 @@ To Action From
-- ------ ----
22 ALLOW Anywhere

172.17.0.2 81/tcp ALLOW FWD Anywhere # crazy_keller:e875afe93296
172.17.0.2 80/tcp ALLOW FWD Anywhere # crazy_keller:e875afe93296
172.17.0.3 82/tcp ALLOW FWD 192.168.3.0 # epic_lederberg:7c5001108663
172.17.0.3 83/tcp ALLOW FWD 192.168.3.0 # epic_lederberg:7c5001108663
172.17.0.4 84/tcp ALLOW FWD 192.168.3.0/24 # beautiful_taussig:089400a84073
172.17.0.4 84/tcp ALLOW FWD 10.10.0.50 # beautiful_taussig:089400a84073
172.17.0.4 85/tcp ALLOW FWD 192.168.3.0/24 # beautiful_taussig:089400a84073
172.17.0.4 85/tcp ALLOW FWD 10.10.0.50 # beautiful_taussig:089400a84073
172.17.0.5 86/tcp ALLOW FWD 172.10.5.0 # funny_aryabhata:9eb642f07bde
172.17.0.5 86/tcp ALLOW FWD 192.168.3.0/24 # funny_aryabhata:9eb642f07bde LAN
172.17.0.5 86/tcp ALLOW FWD 10.10.0.50 # funny_aryabhata:9eb642f07bde DNS
172.17.0.5 87/tcp ALLOW FWD 172.10.5.0 # funny_aryabhata:9eb642f07bde
172.17.0.5 87/tcp ALLOW FWD 192.168.3.0/24 # funny_aryabhata:9eb642f07bde LAN
172.17.0.5 87/tcp ALLOW FWD 10.10.0.50 # funny_aryabhata:9eb642f07bde DNS
172.17.0.6 88/tcp ALLOW FWD Anywhere # awesome_leavitt:6ebdb0c87a56 Internet
172.17.0.6 89/tcp ALLOW FWD 192.168.3.0/24 # awesome_leavitt:6ebdb0c87a56 LAN
172.17.0.6 90/tcp ALLOW FWD 10.10.0.50 # awesome_leavitt:6ebdb0c87a56
172.17.0.2 80/tcp ALLOW FWD Anywhere # pensive_davinci:bb01e89284b6
172.17.0.2 81/tcp ALLOW FWD Anywhere # pensive_davinci:bb01e89284b6
172.17.0.3 83/tcp ALLOW FWD Anywhere # vigorous_wescoff:97403d2d7e08
172.17.0.3 82/tcp ALLOW FWD Anywhere # vigorous_wescoff:97403d2d7e08
Anywhere DENY FWD 172.17.0.3 # vigorous_wescoff:97403d2d7e08
172.17.0.4 84/tcp ALLOW FWD 192.168.3.0 # sweet_poitras:b7f4cbdd363b
172.17.0.4 85/tcp ALLOW FWD 192.168.3.0 # sweet_poitras:b7f4cbdd363b
172.17.0.5 87/tcp ALLOW FWD 192.168.3.0/24 # objective_moore:473a2fd127c4
172.17.0.5 87/tcp ALLOW FWD 10.10.0.50 # objective_moore:473a2fd127c4
172.17.0.5 86/tcp ALLOW FWD 192.168.3.0/24 # objective_moore:473a2fd127c4
172.17.0.5 86/tcp ALLOW FWD 10.10.0.50 # objective_moore:473a2fd127c4
172.17.0.6 89/tcp ALLOW FWD 172.10.5.0 # goofy_dijkstra:3c4d49d8e118
172.17.0.6 89/tcp ALLOW FWD 192.168.3.0/24 # goofy_dijkstra:3c4d49d8e118 LAN
172.17.0.6 89/tcp ALLOW FWD 10.10.0.50 # goofy_dijkstra:3c4d49d8e118 DNS
172.17.0.6 88/tcp ALLOW FWD 172.10.5.0 # goofy_dijkstra:3c4d49d8e118
172.17.0.6 88/tcp ALLOW FWD 192.168.3.0/24 # goofy_dijkstra:3c4d49d8e118 LAN
172.17.0.6 88/tcp ALLOW FWD 10.10.0.50 # goofy_dijkstra:3c4d49d8e118 DNS
172.17.0.7 90/tcp ALLOW FWD 172.10.5.0 # wonderful_wilson:447017665de8
172.17.0.7 90/tcp ALLOW FWD 192.168.3.0/24 # wonderful_wilson:447017665de8 LAN
172.17.0.7 90/tcp ALLOW FWD 10.10.0.50 # wonderful_wilson:447017665de8 DNS
172.17.0.7 91/tcp ALLOW FWD 172.10.5.0 # wonderful_wilson:447017665de8
172.17.0.7 91/tcp ALLOW FWD 192.168.3.0/24 # wonderful_wilson:447017665de8 LAN
172.17.0.7 91/tcp ALLOW FWD 10.10.0.50 # wonderful_wilson:447017665de8 DNS
8.8.8.8 53 ALLOW FWD 172.17.0.7 # wonderful_wilson:447017665de8 GoogleDNS
1.1.1.0/24 53 ALLOW FWD 172.17.0.7 # wonderful_wilson:447017665de8 CloudflareDNS
192.168.10.0/24 ALLOW FWD 172.17.0.7 # wonderful_wilson:447017665de8 LAN
Anywhere DENY FWD 172.17.0.7 # wonderful_wilson:447017665de8
172.17.0.8 88/tcp ALLOW FWD Anywhere # stoic_roentgen:e34a3201c01b Internet
172.17.0.8 89/tcp ALLOW FWD 192.168.3.0/24 # stoic_roentgen:e34a3201c01b LAN
172.17.0.8 90/tcp ALLOW FWD 10.10.0.50 # stoic_roentgen:e34a3201c01b
```

Once containers are stopped their ufw entries will be deleted.
Expand Down Expand Up @@ -189,4 +212,4 @@ sudo systemctl start ufw-docker-automated

## Feedback

If you encounter any issues please feel free to open an issue.
If you encounter any issues please feel free to open an [issue](https://github.com/shinebayar-g/ufw-docker-automated/issues).
123 changes: 107 additions & 16 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,22 @@ func isUfwManaged(containerLabel string) bool {

func handleUfwRule(ch <-chan ufwEvent) {
for event := range ch {
containerIP := event.container.NetworkSettings.IPAddress
// If docker-compose, container IP is defined here
if containerIP == "" {
networkMode := event.container.HostConfig.NetworkMode.NetworkName()
containerIP = event.container.NetworkSettings.Networks[networkMode].IPAddress
}

// Handle inbound rules
for port, portMaps := range event.container.HostConfig.PortBindings {
// List is non empty if port is published
if len(portMaps) > 0 {
ufwSourceList := []ufwSource{}
ufwAllowFromList := []ufwSource{}
if event.msg.Actor.Attributes["UFW_ALLOW_FROM"] != "" {
ufwAllowFromList := strings.Split(event.msg.Actor.Attributes["UFW_ALLOW_FROM"], ";")
ufwAllowFromLabelParsed := strings.Split(event.msg.Actor.Attributes["UFW_ALLOW_FROM"], ";")

for _, allowFrom := range ufwAllowFromList {
for _, allowFrom := range ufwAllowFromLabelParsed {
ip := strings.Split(allowFrom, "-")

if !checkIP(ip[0]) {
Expand All @@ -63,31 +71,24 @@ func handleUfwRule(ch <-chan ufwEvent) {
if len(ip) == 2 {
if _, err := strconv.Atoi(ip[1]); err == nil {
// case: 172.10.5.0-80
ufwSourceList = append(ufwSourceList, ufwSource{CIDR: ip[0], port: ip[1]})
ufwAllowFromList = append(ufwAllowFromList, ufwSource{CIDR: ip[0], port: ip[1]})
} else {
// case: 172.10.5.0-LAN
ufwSourceList = append(ufwSourceList, ufwSource{CIDR: ip[0], comment: fmt.Sprintf(" %s", ip[1])})
ufwAllowFromList = append(ufwAllowFromList, ufwSource{CIDR: ip[0], comment: fmt.Sprintf(" %s", ip[1])})
}
// Example: 172.10.5.0-80-LAN
} else if len(ip) == 3 {
ufwSourceList = append(ufwSourceList, ufwSource{CIDR: ip[0], port: ip[1], comment: fmt.Sprintf(" %s", ip[2])})
ufwAllowFromList = append(ufwAllowFromList, ufwSource{CIDR: ip[0], port: ip[1], comment: fmt.Sprintf(" %s", ip[2])})
// Should be just IP address without comment or port specified.
} else {
ufwSourceList = append(ufwSourceList, ufwSource{CIDR: ip[0]})
ufwAllowFromList = append(ufwAllowFromList, ufwSource{CIDR: ip[0]})
}
}
} else {
ufwSourceList = append(ufwSourceList, ufwSource{CIDR: "any"})
}

containerIP := event.container.NetworkSettings.IPAddress
// If docker-compose, container IP is defined here
if containerIP == "" {
networkMode := event.container.HostConfig.NetworkMode.NetworkName()
containerIP = event.container.NetworkSettings.Networks[networkMode].IPAddress
ufwAllowFromList = append(ufwAllowFromList, ufwSource{CIDR: "any"})
}

for _, source := range ufwSourceList {
for _, source := range ufwAllowFromList {
var cmd *exec.Cmd
var containerPort string

Expand Down Expand Up @@ -124,6 +125,96 @@ func handleUfwRule(ch <-chan ufwEvent) {
// ufw route delete allow proto <tcp|udp> <source> to <container_ip> port <port> comment <comment>
}
}

// Handle outbound rules
if strings.ToUpper(event.msg.Actor.Attributes["UFW_DENY_OUT"]) == "TRUE" {

if event.msg.Actor.Attributes["UFW_ALLOW_TO"] != "" {
ufwAllowToList := []ufwSource{}
ufwAllowToLabelParsed := strings.Split(event.msg.Actor.Attributes["UFW_ALLOW_TO"], ";")

for _, allowTo := range ufwAllowToLabelParsed {
ip := strings.Split(allowTo, "-")

if !checkIP(ip[0]) {
if !checkCIDR(ip[0]) {
fmt.Printf("ufw-docker-automated: Address %s is not valid!\n", ip[0])
continue
}
}

// Example: 172.10.5.0-LAN or 172.10.5.0-80
if len(ip) == 2 {
if _, err := strconv.Atoi(ip[1]); err == nil {
// case: 172.10.5.0-80
ufwAllowToList = append(ufwAllowToList, ufwSource{CIDR: ip[0], port: ip[1]})
} else {
// case: 172.10.5.0-LAN
ufwAllowToList = append(ufwAllowToList, ufwSource{CIDR: ip[0], comment: fmt.Sprintf(" %s", ip[1])})
}
// Example: 172.10.5.0-80-LAN
} else if len(ip) == 3 {
ufwAllowToList = append(ufwAllowToList, ufwSource{CIDR: ip[0], port: ip[1], comment: fmt.Sprintf(" %s", ip[2])})
// Should be just IP address without comment or port specified.
} else {
ufwAllowToList = append(ufwAllowToList, ufwSource{CIDR: ip[0]})
}
}

for _, source := range ufwAllowToList {
var cmd *exec.Cmd

if event.msg.Action == "start" {
if source.port == "" {
cmd = exec.Command("sudo", "ufw", "route", "allow", "from", containerIP, "to", source.CIDR, "comment", event.msg.Actor.Attributes["name"]+":"+event.msg.ID[:12]+source.comment)
} else {
cmd = exec.Command("sudo", "ufw", "route", "allow", "from", containerIP, "to", source.CIDR, "port", source.port, "comment", event.msg.Actor.Attributes["name"]+":"+event.msg.ID[:12]+source.comment)
}
fmt.Println("ufw-docker-automated: Adding rule:", cmd)
} else {
if source.port == "" {
cmd = exec.Command("sudo", "ufw", "route", "delete", "allow", "from", containerIP, "to", source.CIDR, "comment", event.msg.Actor.Attributes["name"]+":"+event.msg.ID[:12]+source.comment)
} else {
cmd = exec.Command("sudo", "ufw", "route", "delete", "allow", "from", containerIP, "to", source.CIDR, "port", source.port, "comment", event.msg.Actor.Attributes["name"]+":"+event.msg.ID[:12]+source.comment)
}
fmt.Println("ufw-docker-automated: Deleting rule:", cmd)
}

var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()

if err != nil || stderr.String() != "" {
fmt.Println("ufw:", err, stderr.String())
} else {
fmt.Println("ufw:", stdout.String())
}
}
}

// Handle deny all out
var cmd *exec.Cmd

if event.msg.Action == "start" {
cmd = exec.Command("sudo", "ufw", "route", "deny", "from", containerIP, "to", "any", "comment", event.msg.Actor.Attributes["name"]+":"+event.msg.ID[:12])
fmt.Println("ufw-docker-automated: Adding rule:", cmd)
} else {
cmd = exec.Command("sudo", "ufw", "route", "delete", "deny", "from", containerIP, "to", "any", "comment", event.msg.Actor.Attributes["name"]+":"+event.msg.ID[:12])
fmt.Println("ufw-docker-automated: Deleting rule:", cmd)
}

var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()

if err != nil || stderr.String() != "" {
fmt.Println("ufw:", err, stderr.String())
} else {
fmt.Println("ufw:", stdout.String())
}
}
}
}

Expand Down

0 comments on commit e6ad513

Please sign in to comment.