netfilter: bridge: use rcu hook to resolve br_netfilter dependency
[cascardo/linux.git] / net / bridge / br_netfilter.c
index 0ee453f..b260a97 100644 (file)
@@ -239,6 +239,14 @@ drop:
        return -1;
 }
 
+static void nf_bridge_update_protocol(struct sk_buff *skb)
+{
+       if (skb->nf_bridge->mask & BRNF_8021Q)
+               skb->protocol = htons(ETH_P_8021Q);
+       else if (skb->nf_bridge->mask & BRNF_PPPoE)
+               skb->protocol = htons(ETH_P_PPP_SES);
+}
+
 /* PF_BRIDGE/PRE_ROUTING *********************************************/
 /* Undo the changes made for ip6tables PREROUTING and continue the
  * bridge PRE_ROUTING hook. */
@@ -764,23 +772,53 @@ static unsigned int br_nf_forward_arp(const struct nf_hook_ops *ops,
 }
 
 #if IS_ENABLED(CONFIG_NF_DEFRAG_IPV4)
+static bool nf_bridge_copy_header(struct sk_buff *skb)
+{
+       int err;
+       unsigned int header_size;
+
+       nf_bridge_update_protocol(skb);
+       header_size = ETH_HLEN + nf_bridge_encap_header_len(skb);
+       err = skb_cow_head(skb, header_size);
+       if (err)
+               return false;
+
+       skb_copy_to_linear_data_offset(skb, -header_size,
+                                      skb->nf_bridge->data, header_size);
+       __skb_push(skb, nf_bridge_encap_header_len(skb));
+       return true;
+}
+
+static int br_nf_push_frag_xmit(struct sk_buff *skb)
+{
+       if (!nf_bridge_copy_header(skb)) {
+               kfree_skb(skb);
+               return 0;
+       }
+
+       return br_dev_queue_push_xmit(skb);
+}
+
 static int br_nf_dev_queue_xmit(struct sk_buff *skb)
 {
        int ret;
        int frag_max_size;
+       unsigned int mtu_reserved;
+
+       if (skb_is_gso(skb) || skb->protocol != htons(ETH_P_IP))
+               return br_dev_queue_push_xmit(skb);
 
+       mtu_reserved = nf_bridge_mtu_reduction(skb);
        /* This is wrong! We should preserve the original fragment
         * boundaries by preserving frag_list rather than refragmenting.
         */
-       if (skb->protocol == htons(ETH_P_IP) &&
-           skb->len + nf_bridge_mtu_reduction(skb) > skb->dev->mtu &&
-           !skb_is_gso(skb)) {
+       if (skb->len + mtu_reserved > skb->dev->mtu) {
                frag_max_size = BR_INPUT_SKB_CB(skb)->frag_max_size;
                if (br_parse_ip_options(skb))
                        /* Drop invalid packet */
                        return NF_DROP;
                IPCB(skb)->frag_max_size = frag_max_size;
-               ret = ip_fragment(skb, br_dev_queue_push_xmit);
+               ret = ip_fragment(skb, br_nf_push_frag_xmit);
        } else
                ret = br_dev_queue_push_xmit(skb);
 
@@ -854,6 +892,41 @@ static unsigned int ip_sabotage_in(const struct nf_hook_ops *ops,
        return NF_ACCEPT;
 }
 
+/* This is called when br_netfilter has called into iptables/netfilter,
+ * and DNAT has taken place on a bridge-forwarded packet.
+ *
+ * neigh->output has created a new MAC header, with local br0 MAC
+ * as saddr.
+ *
+ * This restores the original MAC saddr of the bridged packet
+ * before invoking bridge forward logic to transmit the packet.
+ */
+static void br_nf_pre_routing_finish_bridge_slow(struct sk_buff *skb)
+{
+       struct nf_bridge_info *nf_bridge = skb->nf_bridge;
+
+       skb_pull(skb, ETH_HLEN);
+       nf_bridge->mask &= ~BRNF_BRIDGED_DNAT;
+
+       skb_copy_to_linear_data_offset(skb, -(ETH_HLEN-ETH_ALEN),
+                                      skb->nf_bridge->data, ETH_HLEN-ETH_ALEN);
+       skb->dev = nf_bridge->physindev;
+       br_handle_frame_finish(skb);
+}
+
+static int br_nf_dev_xmit(struct sk_buff *skb)
+{
+       if (skb->nf_bridge && (skb->nf_bridge->mask & BRNF_BRIDGED_DNAT)) {
+               br_nf_pre_routing_finish_bridge_slow(skb);
+               return 1;
+       }
+       return 0;
+}
+
+static const struct nf_br_ops br_ops = {
+       .br_dev_xmit_hook =     br_nf_dev_xmit,
+};
+
 void br_netfilter_enable(void)
 {
 }
@@ -991,12 +1064,14 @@ static int __init br_netfilter_init(void)
                return -ENOMEM;
        }
 #endif
+       RCU_INIT_POINTER(nf_br_ops, &br_ops);
        printk(KERN_NOTICE "Bridge firewalling registered\n");
        return 0;
 }
 
 static void __exit br_netfilter_fini(void)
 {
+       RCU_INIT_POINTER(nf_br_ops, NULL);
        nf_unregister_hooks(br_nf_ops, ARRAY_SIZE(br_nf_ops));
 #ifdef CONFIG_SYSCTL
        unregister_net_sysctl_table(brnf_sysctl_header);